diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index a059cffe2..3933181fb 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -12654,6 +12654,12 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" + "413": + description: The request body is over 16 KB. Nothing was generated. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" "422": description: No AI provider can serve the `briefing` capability (`meta.errorCode` = `ai.provider.none`, `meta.reason` = `no_provider`), or the provider answered something that was not valid JSON (`meta.errorCode` = @@ -12672,6 +12678,13 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" + "502": + description: The generated briefing did not pass the outbound safety screen and was discarded (`meta.errorCode` = + `insights.generate.outboundScreened`). Regenerating may succeed. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" "503": description: "Every provider in the chain failed for a reason that is not a rate limit — auth, timeout or an upstream 5xx. A dated failure marker is written, so the read GET reports the briefing as held rather than fresh. @@ -19188,6 +19201,13 @@ paths: $ref: "#/components/schemas/DocumentUploadEnvelope" "401": *a1 "403": *a9 + "404": + description: An `episodeIds` or `encounterIds` id is not a live episode or visit of the caller + (`documents.inbound.episodeNotFound` / `documents.inbound.encounterNotFound`). Nothing was stored. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" "409": description: The same bytes are already stored under 20 other import source ids (`documents.inbound.sourceAliasLimit`). Nothing was stored or remembered. @@ -50492,8 +50512,28 @@ components: anyOf: - type: array items: - type: string + anyOf: + - type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + - type: object + properties: + t: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + speedMs: + type: number + hr: + type: integer + minimum: -9007199254740991 + maximum: 9007199254740991 + required: + - t + additionalProperties: false - type: "null" + description: "One entry per geometry point, as the track was stored: a bare instant, or (from the workout batch and the + Apple Health export) an object with its instant and, where recorded, speed (m/s) and heart rate (bpm)." required: - geometry - sampleTimestamps @@ -78976,6 +79016,15 @@ components: properties: requestId: type: string + duplicate: + description: "Present on a 200: the bytes or the source key were already stored, and `data` is the existing row (or the + receipt)." + type: boolean + const: true + deleted: + description: Present with `duplicate` when the source key belongs to a document the owner deleted. Nothing was stored. + type: boolean + const: true additionalProperties: false required: - data diff --git a/src/__tests__/ai-egress-capability-guard.test.ts b/src/__tests__/ai-egress-capability-guard.test.ts index 2b84a83b4..403e64687 100644 --- a/src/__tests__/ai-egress-capability-guard.test.ts +++ b/src/__tests__/ai-egress-capability-guard.test.ts @@ -312,7 +312,7 @@ describe("workout route geometry never reaches an AI path", () => { /\breadRouteGeometry\s*\(|\bencryptRouteGeometry\s*\(|\bgeometryEncrypted\b|\bgeometry\s*:\s*true\b/; const ROUTE_READERS: Record = { - "app/api/workouts/[id]/route.ts": + "lib/workouts/detail.ts": "The workout detail read: serves the track to the person's own map.", "lib/workouts/batch-ingest/write.ts": "The workout batch ingest's write: stores a kept track (opt-in), sealed.", diff --git a/src/__tests__/background-reasoning-inventory.test.ts b/src/__tests__/background-reasoning-inventory.test.ts index ccb773f6c..0354a6a87 100644 --- a/src/__tests__/background-reasoning-inventory.test.ts +++ b/src/__tests__/background-reasoning-inventory.test.ts @@ -41,7 +41,7 @@ const ADMITTED: Record = { jobs: ["daily_briefing"], why: "The daily briefing's generation and its phrasing re-roll; the JSON and grounding repairs do not reason.", }, - "app/api/insights/generate/route.ts": { + "lib/insights/briefing-request/completion.ts": { jobs: ["daily_briefing"], why: "A briefing asked for by hand reasons like the scheduled one; its JSON and grounding repairs do not.", }, diff --git a/src/__tests__/baselines/source-file-lines.json b/src/__tests__/baselines/source-file-lines.json index 276047272..798eba59d 100644 --- a/src/__tests__/baselines/source-file-lines.json +++ b/src/__tests__/baselines/source-file-lines.json @@ -1,13 +1,8 @@ { "$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.", - "count": 96, - "total": 106730, + "count": 91, + "total": 101359, "entries": { - "app/api/dashboard/summary/route.ts": 1032, - "app/api/documents/inbound/route.ts": 811, - "app/api/insights/generate/route.ts": 1214, - "app/api/medications/[id]/route.ts": 1270, - "app/api/medications/intake/bulk/route.ts": 1044, "app/medications/page-client.tsx": 839, "components/charts/mood-chart.tsx": 1166, "components/cycle/log-day-sheet.tsx": 1639, diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json index a5763cc6d..d7c1b5530 100644 --- a/src/__tests__/baselines/source-function-lines.json +++ b/src/__tests__/baselines/source-function-lines.json @@ -1,35 +1,27 @@ { "$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.", - "count": 373, - "total": 124458, + "count": 355, + "total": 114858, "entries": { "app/achievements/page.tsx :: AchievementsPage": 206, "app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 314, "app/api/admin/notifications/reminder-check/route.ts :: apiHandler(arg)": 208, "app/api/admin/settings/route.ts :: apiHandler(arg) #2": 211, - "app/api/analytics/route.ts :: buildAnalyticsResponse": 446, "app/api/auth/login/route.ts :: apiHandler(arg)": 262, "app/api/auth/me/route.ts :: apiHandler(arg)": 386, - "app/api/auth/oidc/callback/route.ts :: apiHandler(arg)": 499, + "app/api/auth/oidc/callback/route.ts :: apiHandler(arg)": 294, "app/api/auth/register/route.ts :: apiHandler(arg)": 268, "app/api/coach/about-me/route.ts :: apiHandler(arg) #2": 309, "app/api/cycle/calendar/route.ts :: apiHandler(arg)": 230, "app/api/cycle/day-logs/bulk/route.ts :: postBulk": 213, "app/api/cycle/insights/route.ts :: apiHandler(arg)": 226, - "app/api/dashboard/summary/route.ts :: buildDashboardSummary": 606, "app/api/devices/route.ts :: apiHandler(arg)": 231, "app/api/documents/inbound/bulk/route.ts :: apiHandler(arg)": 207, - "app/api/documents/inbound/route.ts :: processUpload": 309, "app/api/encounters/[id]/route.ts :: apiHandler(arg) #2": 269, "app/api/environment/route.ts :: apiHandler(arg)": 201, "app/api/export/health-record/route.ts :: apiHandler(arg)": 241, - "app/api/import/csv/route.ts :: apiHandler(arg)": 407, - "app/api/import/route.ts :: apiHandler(arg)": 364, "app/api/ingest/medication/route.ts :: apiHandler(arg)": 267, "app/api/insights/cards/route.ts :: apiHandler(arg)": 211, - "app/api/insights/comprehensive/route.ts :: buildComprehensiveResponse": 505, - "app/api/insights/generate/route.ts :: apiHandler(arg) #2": 866, - "app/api/insights/generate/route.ts :: apiHandler(arg) > withFeatureCacheScope(arg)": 862, "app/api/integrations/status/route.ts :: apiHandler(arg)": 451, "app/api/labs/[id]/route.ts :: apiHandler(arg) #2": 239, "app/api/labs/ocr/commit/route.ts :: commitOcrRows": 285, @@ -37,28 +29,18 @@ "app/api/mcp/oauth/token/route.ts :: POST": 250, "app/api/mcp/oauth/token/route.ts :: POST > withBackgroundEvent(arg)": 248, "app/api/measurement-reminders/[id]/route.ts :: apiHandler(arg) #2": 239, - "app/api/measurements/series/route.ts :: apiHandler(arg)": 549, "app/api/medications/[id]/dose-history/route.ts :: apiHandler(arg)": 256, - "app/api/medications/[id]/intake/route.ts :: postIntake": 607, "app/api/medications/[id]/inventory/[itemId]/route.ts :: apiHandler(arg)": 204, - "app/api/medications/[id]/route.ts :: apiHandler(arg)": 214, - "app/api/medications/[id]/route.ts :: apiHandler(arg) #2": 886, - "app/api/medications/[id]/route.ts :: apiHandler(arg) > prisma.$transaction(arg)": 293, "app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts :: apiHandler(arg)": 250, "app/api/medications/extract/route.ts :: handleExtract": 201, - "app/api/medications/intake/bulk/route.ts :: postBulk": 846, - "app/api/medications/intake/route.ts :: apiHandler(arg) #2": 432, "app/api/mental-health/assessments/route.ts :: postAssessment": 249, "app/api/mood-entries/[id]/route.ts :: apiHandler(arg) #2": 296, - "app/api/mood-entries/bulk/route.ts :: postBulk": 479, "app/api/mood-entries/route.ts :: postMoodEntry": 324, "app/api/nutrients/batch/route.ts :: postBatch": 304, "app/api/onboarding/complete/route.ts :: completeNeedsFlow": 218, "app/api/record-settings/[family]/route.ts :: apiHandler(arg) #2": 262, - "app/api/sync/changes/route.ts :: apiHandler(arg)": 427, "app/api/user/ai-provider/route.ts :: apiHandler(arg) #2": 248, "app/api/withings/callback/route.ts :: apiHandler(arg)": 247, - "app/api/workouts/[id]/route.ts :: apiHandler(arg)": 297, "app/auth/login/page.tsx :: LoginPage": 576, "app/auth/register/page.tsx :: RegisterPage": 285, "app/coach/conversations/page.tsx :: CoachConversationsBody": 249, diff --git a/src/__tests__/baselines/version-stamp-comments.json b/src/__tests__/baselines/version-stamp-comments.json index 707236cf0..32b18fa49 100644 --- a/src/__tests__/baselines/version-stamp-comments.json +++ b/src/__tests__/baselines/version-stamp-comments.json @@ -1,7 +1,7 @@ { "$comment": "Comment lines that open with a release version, per file under src/. Maintained by src/__tests__/version-stamp-ratchet.test.ts.", - "count": 2904, - "total": 9630, + "count": 2898, + "total": 9297, "entries": { "__tests__/account-payload-consumer-guard.test.ts": 2, "__tests__/acting-account-boundary-guard.test.ts": 7, @@ -128,7 +128,7 @@ "app/api/allergies/route.ts": 1, "app/api/analytics/__tests__/route.test.ts": 27, "app/api/analytics/range/route.ts": 2, - "app/api/analytics/route.ts": 39, + "app/api/analytics/route.ts": 10, "app/api/anamnesis/facts/__tests__/route.test.ts": 1, "app/api/auth/claim/__tests__/route.test.ts": 1, "app/api/auth/claim/preview/route.ts": 1, @@ -163,7 +163,7 @@ "app/api/auth/mfa/verify/route.ts": 1, "app/api/auth/mfa/webauthn/verify/route.ts": 1, "app/api/auth/oidc/callback/__tests__/route.test.ts": 1, - "app/api/auth/oidc/callback/route.ts": 7, + "app/api/auth/oidc/callback/route.ts": 3, "app/api/auth/oidc/login/route.ts": 2, "app/api/auth/passkey/login-options/route.ts": 1, "app/api/auth/passkey/login-verify/__tests__/native-token.test.ts": 2, @@ -220,7 +220,7 @@ "app/api/dashboard/snapshot/__tests__/route.test.ts": 2, "app/api/dashboard/snapshot/route.ts": 1, "app/api/dashboard/summary/__tests__/route.test.ts": 16, - "app/api/dashboard/summary/route.ts": 31, + "app/api/dashboard/summary/route.ts": 5, "app/api/dashboard/widgets/__tests__/route.test.ts": 4, "app/api/dashboard/widgets/route.ts": 14, "app/api/devices/__tests__/route.test.ts": 1, @@ -243,7 +243,7 @@ "app/api/documents/inbound/[id]/summary/route.ts": 2, "app/api/documents/inbound/[id]/thumbnail/route.ts": 1, "app/api/documents/inbound/reindex/route.ts": 1, - "app/api/documents/inbound/route.ts": 3, + "app/api/documents/inbound/route.ts": 2, "app/api/documents/inbound/usage/route.ts": 1, "app/api/environment/backfill/__tests__/route.test.ts": 1, "app/api/environment/route.ts": 1, @@ -284,7 +284,7 @@ "app/api/illness/episodes/route.ts": 1, "app/api/import/__tests__/route.test.ts": 3, "app/api/import/csv/__tests__/route.test.ts": 2, - "app/api/import/route.ts": 8, + "app/api/import/route.ts": 4, "app/api/ingest/medication/__tests__/route.test.ts": 1, "app/api/ingest/medication/route.ts": 7, "app/api/insights/biomarker-assessment/route.ts": 1, @@ -321,7 +321,6 @@ "app/api/insights/coach/nudge-status/route.ts": 1, "app/api/insights/coach/seen/route.ts": 1, "app/api/insights/comprehensive/__tests__/route.test.ts": 9, - "app/api/insights/comprehensive/route.ts": 24, "app/api/insights/correlations/__tests__/route.test.ts": 10, "app/api/insights/correlations/route.ts": 9, "app/api/insights/derived/__tests__/route.test.ts": 4, @@ -334,7 +333,6 @@ "app/api/insights/ecg/route.ts": 1, "app/api/insights/feedback/route.ts": 2, "app/api/insights/generate/__tests__/route.test.ts": 12, - "app/api/insights/generate/route.ts": 57, "app/api/insights/glp1-plateau/route.ts": 2, "app/api/insights/glp1-timeline/route.ts": 4, "app/api/insights/health-status/route.ts": 3, @@ -413,7 +411,7 @@ "app/api/measurements/series-batch/__tests__/route.test.ts": 1, "app/api/measurements/series-batch/route.ts": 1, "app/api/measurements/series/__tests__/route.test.ts": 5, - "app/api/measurements/series/route.ts": 21, + "app/api/measurements/series/route.ts": 6, "app/api/medications/__tests__/route.test.ts": 10, "app/api/medications/[id]/__tests__/route.test.ts": 8, "app/api/medications/[id]/api-endpoint/route.ts": 2, @@ -434,11 +432,9 @@ "app/api/medications/[id]/intake/import/__tests__/route.test.ts": 2, "app/api/medications/[id]/intake/import/route.ts": 6, "app/api/medications/[id]/intake/purge/route.ts": 4, - "app/api/medications/[id]/intake/route.ts": 30, "app/api/medications/[id]/inventory/[itemId]/route.ts": 8, "app/api/medications/[id]/inventory/route.ts": 4, "app/api/medications/[id]/phase-config/route.ts": 4, - "app/api/medications/[id]/route.ts": 58, "app/api/medications/[id]/schedule-revisions/__tests__/route.test.ts": 1, "app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts": 9, "app/api/medications/[id]/schedule-revisions/route.ts": 3, @@ -453,9 +449,8 @@ "app/api/medications/intake/__tests__/route.test.ts": 21, "app/api/medications/intake/bulk/__tests__/dose-arrival-scope.test.ts": 1, "app/api/medications/intake/bulk/__tests__/route.test.ts": 10, - "app/api/medications/intake/bulk/route.ts": 42, + "app/api/medications/intake/bulk/route.ts": 9, "app/api/medications/intake/dose-history-import/route.ts": 2, - "app/api/medications/intake/route.ts": 34, "app/api/medications/layout/__tests__/route.test.ts": 3, "app/api/medications/layout/route.ts": 3, "app/api/medications/route.ts": 2, @@ -467,7 +462,7 @@ "app/api/mood-entries/bulk-delete/__tests__/route.test.ts": 1, "app/api/mood-entries/bulk-delete/route.ts": 4, "app/api/mood-entries/bulk/__tests__/route.test.ts": 5, - "app/api/mood-entries/bulk/route.ts": 16, + "app/api/mood-entries/bulk/route.ts": 6, "app/api/mood-entries/restore/__tests__/route.test.ts": 1, "app/api/mood-entries/restore/route.ts": 3, "app/api/mood-entries/route.ts": 22, @@ -533,7 +528,6 @@ "app/api/strava/credentials/route.ts": 1, "app/api/strava/disconnect/route.ts": 1, "app/api/strava/status/route.ts": 1, - "app/api/sync/changes/route.ts": 5, "app/api/sync/state/route.ts": 2, "app/api/telegram/webhook/__tests__/route.test.ts": 3, "app/api/user/ai-provider/__tests__/legacy-private-host.test.ts": 1, @@ -559,7 +553,7 @@ "app/api/withings/webhook/route.ts": 2, "app/api/workouts/__tests__/batch-create.test.ts": 3, "app/api/workouts/__tests__/canonical-dedup.test.ts": 3, - "app/api/workouts/[id]/route.ts": 5, + "app/api/workouts/[id]/route.ts": 2, "app/api/workouts/batch/route.ts": 3, "app/api/workouts/route.ts": 2, "app/auth/claim/page.tsx": 1, diff --git a/src/__tests__/dashboard-summary-read-plan-guard.test.ts b/src/__tests__/dashboard-summary-read-plan-guard.test.ts index ce7035eb5..6b582f7fe 100644 --- a/src/__tests__/dashboard-summary-read-plan-guard.test.ts +++ b/src/__tests__/dashboard-summary-read-plan-guard.test.ts @@ -25,7 +25,8 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; const READS = join(process.cwd(), "src/lib/dashboard/summary-reads.ts"); -const ROUTE = join(process.cwd(), "src/app/api/dashboard/summary/route.ts"); +/** The summary's builder, moved out of its route; it makes the reads. */ +const ROUTE = join(process.cwd(), "src/lib/dashboard/summary/build.ts"); /** * Source with every comment removed, so a guard matches the SQL the diff --git a/src/__tests__/dense-measurement-read-guard.test.ts b/src/__tests__/dense-measurement-read-guard.test.ts index 1134cdf0d..dc52ec8a1 100644 --- a/src/__tests__/dense-measurement-read-guard.test.ts +++ b/src/__tests__/dense-measurement-read-guard.test.ts @@ -69,6 +69,15 @@ const SCOPED_DIRS = [ "lib/measurements/batch-ingest/", "lib/measurements/create/", "lib/measurements/list/measurement-list.ts", + "lib/medications/read-medication.ts", + "lib/medications/update-medication/", + "lib/medications/delete-medication.ts", + "lib/medications/record-intake/", + "lib/medications/list-intake-events.ts", + "lib/medications/intake-today.ts", + "lib/medications/intake-compliance.ts", + "lib/medications/update-intake-status/", + "lib/medications/intake-bulk/", ]; /** Types a device writes at sampling rate rather than a few times a day. */ @@ -106,7 +115,7 @@ const ALLOWED: Record = { "Same 30-day clinical glucose panel as the coach snapshot (per-reading metrics), grouped by meal context.", "lib/analytics/score/reader.ts::{ in: types": "Health-score inputs: steps (drained to one row per day nightly), sleep, waist, weight, blood pressure, fasting glucose only.", - 'app/api/analytics/route.ts::"BLOOD_GLUCOSE"': + 'lib/analytics/response/glucose.ts::"BLOOD_GLUCOSE"': "The 30-day per-context glucose summary, the same window and bound as the dashboard and coach glucose panels: at a sensor's fixed rate, below 45 000 rows.", "lib/measurements/batch-ingest/write-measurements.ts::p.row.type as MeasurementType": "Cross-source merge probe: one ±tolerance window around each posted reading's instant, so the rows found are bounded by the batch size.", diff --git a/src/__tests__/external-id-stability-guard.test.ts b/src/__tests__/external-id-stability-guard.test.ts index 75b8eb974..9735cd739 100644 --- a/src/__tests__/external-id-stability-guard.test.ts +++ b/src/__tests__/external-id-stability-guard.test.ts @@ -365,7 +365,7 @@ describe("the intake idempotency key is never read across users", () => { const found = intakeQueriesFilteringOnTheKey(); expect(found.length).toBeGreaterThanOrEqual(6); expect(found.map((f) => f.file)).toContain( - "app/api/medications/intake/bulk/route.ts", + "lib/medications/intake-bulk/entry-write.ts", ); }); @@ -402,14 +402,21 @@ describe("the batch surfaces refuse per entry, never whole-batch", () => { "lib/measurements/batch-ingest/prepare.ts", "lib/measurements/batch-ingest/ingest.ts", ], - "app/api/mood-entries/bulk/route.ts": [], - "app/api/medications/intake/bulk/route.ts": [], + "app/api/mood-entries/bulk/route.ts": ["lib/mood/bulk-ingest.ts"], + "app/api/medications/intake/bulk/route.ts": [ + "lib/medications/intake-bulk/entry.ts", + "lib/medications/intake-bulk/ingest.ts", + ], "app/api/workouts/batch/route.ts": [ "lib/workouts/batch-ingest/entries.ts", "lib/workouts/batch-ingest/ingest.ts", ], "app/api/cycle/day-logs/bulk/route.ts": [], - "app/api/import/route.ts": [], + "app/api/import/route.ts": [ + "lib/import/json/measurements.ts", + "lib/import/json/mood.ts", + "lib/import/json/run.ts", + ], }; const BATCH_ROUTES = Object.keys(BATCH_SURFACES); diff --git a/src/__tests__/manage-activity-inventory.test.ts b/src/__tests__/manage-activity-inventory.test.ts index 0a69a99b3..f80af878d 100644 --- a/src/__tests__/manage-activity-inventory.test.ts +++ b/src/__tests__/manage-activity-inventory.test.ts @@ -309,9 +309,44 @@ function actionsReachableFrom( out.push(...packageActions(reexported, read)); } } + for (const body of HANDLER_BODY_MODULES[relative(SRC, file)] ?? []) { + out.push(...actionsReachableFrom(join(SRC, body), read)); + } return out; } +/** + * Delegated routes whose handler bodies live in service modules under + * `src/lib/`, keyed by the route relative to `src/`. A handler body is the + * route's own code moved, so it counts at the route's level: its actions and + * those of its direct imports (the medication lifecycle a dose write + * reconciles through) are reached as they were before the move. Each module + * has to be a direct import of its route (leg below), so an entry cannot + * outlive the delegation it describes. + */ +const HANDLER_BODY_MODULES: Record = { + "app/api/medications/route.ts": ["lib/medications/create-medication.ts"], + "app/api/medications/[id]/route.ts": [ + "lib/medications/read-medication.ts", + "lib/medications/update-medication/update-medication.ts", + "lib/medications/delete-medication.ts", + ], + "app/api/medications/[id]/intake/route.ts": [ + "lib/medications/record-intake/record-intake.ts", + "lib/medications/list-intake-events.ts", + ], + "app/api/medications/[id]/intake/[eventId]/route.ts": [ + "lib/medications/update-intake-event.ts", + "lib/medications/delete-intake-event.ts", + ], + "app/api/medications/intake/route.ts": [ + "lib/medications/intake-today.ts", + "lib/medications/intake-compliance.ts", + "lib/medications/update-intake-status/admission.ts", + "lib/medications/update-intake-status/update-intake-status.ts", + ], +}; + /** * The actions of a re-exported module together with the sibling modules it * imports from its own folder. The re-exported module is a package's public @@ -553,6 +588,21 @@ describe("every delegated action has a sentence of its own", () => { expect(manageOnly).not.toContain(anchor); }); + it("reads each moved handler body where its route imports it from", () => { + const entries = Object.entries(HANDLER_BODY_MODULES); + expect(entries.length).toBeGreaterThan(0); + for (const [route, bodies] of entries) { + const file = join(SRC, route); + expect(delegatedModules, route).toContain(file); + for (const body of bodies) { + expect( + read(file).imports, + `${route} does not import ${body}`, + ).toContain(join(SRC, body)); + } + } + }); + it("reaches the actions a route files through what it imports", () => { // Both halves of the fix, pinned against the two that were invisible. expect(actions).toContain("medication.intake.import.kickoff.denied"); diff --git a/src/__tests__/measurement-write-plausibility-guard.test.ts b/src/__tests__/measurement-write-plausibility-guard.test.ts index 12c9de285..0de1ac9ce 100644 --- a/src/__tests__/measurement-write-plausibility-guard.test.ts +++ b/src/__tests__/measurement-write-plausibility-guard.test.ts @@ -73,7 +73,10 @@ const WRITERS: Record = { kind: "upstream", validatedIn: "lib/validations/measurement.ts", }, - "app/api/import/route.ts": { kind: "validator" }, + "lib/import/json/measurements.ts": { + kind: "upstream", + validatedIn: "app/api/import/route.ts", + }, // The batch ingest's set-based insert writes rows that already passed // `validateMeasurementRange` per entry in its first pass; everything else // goes through the reconciler, which gates itself. @@ -88,7 +91,7 @@ const WRITERS: Record = { // insert after `validateMeasurementRange`; day totals through the gated // reconcile seam. "lib/import/health-connect/import.ts": { kind: "validator" }, - "app/api/import/csv/route.ts": { + "lib/import/csv-write.ts": { kind: "upstream", validatedIn: "lib/import/csv-measurements.ts", }, diff --git a/src/__tests__/measurement-write-tail-guard.test.ts b/src/__tests__/measurement-write-tail-guard.test.ts index 9a7f671a4..66e358b74 100644 --- a/src/__tests__/measurement-write-tail-guard.test.ts +++ b/src/__tests__/measurement-write-tail-guard.test.ts @@ -52,8 +52,8 @@ const PROVIDER_TAIL = "runs afterProviderWrite"; * fails too). */ const WRITE_SITES: Record = { - "app/api/import/csv/route.ts": TAIL, - "app/api/import/route.ts": TAIL, + "lib/import/csv-write.ts": TAIL, + "lib/import/json/measurements.ts": TAIL_VIA("lib/import/json/run.ts"), "app/api/measurements/[id]/route.ts": TAIL, "app/api/measurements/bulk-delete/route.ts": TAIL, "app/api/measurements/by-external-ids/route.ts": TAIL, diff --git a/src/__tests__/medication-intake-tracking-guard.test.ts b/src/__tests__/medication-intake-tracking-guard.test.ts index 5638ee82f..0558a4887 100644 --- a/src/__tests__/medication-intake-tracking-guard.test.ts +++ b/src/__tests__/medication-intake-tracking-guard.test.ts @@ -30,7 +30,7 @@ import { describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; import { join } from "node:path"; -import { walkSourceFiles } from "./helpers/source-files"; +import { stripComments, walkSourceFiles } from "./helpers/source-files"; const SRC = join(process.cwd(), "src"); @@ -57,22 +57,24 @@ const INTAKE_READ = */ const INTAKE_MAY_SEE_EVERY_ROW: Record = { "app/api/admin/status/route.ts": "instance row counts", - "app/api/dashboard/summary/route.ts": + "lib/dashboard/summary/build.ts": "engagement streak of days with any logged action, no rate", "app/api/export/medications/route.ts": "export of the recorded history", "app/api/export/route.ts": "export of the recorded history", "lib/export/medications-backup.ts": "backup of the recorded history", "app/api/sync/state/route.ts": "sync watermarks, no rate", + "lib/sync/changes/read-rows.ts": + "delta feed mirroring the recorded rows to a paired client, no rate", "lib/jobs/measurement-tombstone-cleanup.ts": "tombstone purge", "lib/ai/coach/tools/availability.ts": "whether any intake history exists, and its date range", - "app/api/medications/[id]/intake/route.ts": - "one medication's recorded history, listed or written", + "lib/medications/list-intake-events.ts": + "one medication's recorded history, listed", "app/api/medications/[id]/intake/bulk-delete/route.ts": "deletes rows the person chose", - "app/api/medications/[id]/route.ts": - "one medication's slots; tombstones on the tracking switch", - "app/api/medications/intake/bulk/route.ts": "writes recorded doses", + "lib/medications/update-medication/write.ts": + "one medication's open slots, tombstoned on a schedule replace or the tracking switch", + "lib/medications/intake-bulk/ingest.ts": "writes recorded doses", "lib/medications/scheduling/slot-upsert.ts": "binds a recorded dose to its slot", "lib/medications/scheduling/worker-helpers.ts": @@ -113,6 +115,14 @@ const MAY_READ_EVERY_ROW: Record = { "units consumed by a recorded dose", "lib/medications/intake-slot-dedup.ts": "folds duplicate rows of doses already recorded", + // The medication edit: the schedule count and the primary schedule's id + // for the reminder-window bridge, and the replaced rows archived as an era + // whose payload is empty for a stretch with tracking off. Nothing due comes + // out of either; the edit answers through scheduleWireFields. + "lib/medications/update-medication/preflight.ts": + "schedule count and the primary schedule's id, derives nothing due", + "lib/medications/update-medication/write.ts": + "archives the replaced schedule; the era payload follows the tracking switch", // Readers that hand the rows to a builder whose input TYPE requires // `trackIntake` and which gates on it: the per-medication compliance // payload (`buildCompliancePayload`, INTAKE_NOT_TRACKED) and the doctor @@ -141,12 +151,6 @@ function sourceFiles(): string[] { } /** Drop line and block comments so a mention in prose never counts. */ -function stripComments(source: string): string { - return source - .replace(/\/\*[\s\S]*?\*\//g, "") - .replace(/(^|[^:])\/\/.*$/gm, "$1"); -} - function code(rel: string): string { return stripComments(readFileSync(join(SRC, rel), "utf8")); } diff --git a/src/__tests__/oidc-native-error-reasons-documented.test.ts b/src/__tests__/oidc-native-error-reasons-documented.test.ts index b5b7ec7c9..92a5e1dbd 100644 --- a/src/__tests__/oidc-native-error-reasons-documented.test.ts +++ b/src/__tests__/oidc-native-error-reasons-documented.test.ts @@ -37,10 +37,13 @@ describe("native OIDC error reasons are documented", () => { // The login route answers both legs through `loginError`; the callback's // native-aware helper is `failRedirect`. `errorRedirect` in the callback is // the browser-only branch taken before the state (and so the native flag) - // could be read, and never reaches the app. + // could be read, and never reaches the app. The account resolution the + // callback calls names its refusals through `refuse`, and the callback + // hands each one to `failRedirect`. const sent = new Set([ ...reasons(read("app/api/auth/oidc/login/route.ts"), ["loginError"]), ...reasons(read("app/api/auth/oidc/callback/route.ts"), ["failRedirect"]), + ...reasons(read("lib/auth/oidc-callback/resolve-account.ts"), ["refuse"]), ]); // The login route's 302 description is one string literal on one line. diff --git a/src/__tests__/pulse-day-mean-reader-guard.test.ts b/src/__tests__/pulse-day-mean-reader-guard.test.ts index 50fa574b0..049725d57 100644 --- a/src/__tests__/pulse-day-mean-reader-guard.test.ts +++ b/src/__tests__/pulse-day-mean-reader-guard.test.ts @@ -70,7 +70,9 @@ const SQL_MEAN = const TS_MEAN = /\.reduce\(\s*\(\s*\w+\s*,\s*\w+\s*\)\s*=>\s*\w+\s*\+\s*[\w.]+\s*,\s*0\s*\)\s*\/\s*[\w.]+\.length|\bavgInWindow\s*\(|\bsummarize\s*\(|\bbucketWeekly\s*\(|\bbuildDailyValueRows\s*\(|\bapplyPayloadBudget\s*\(/g; -const NAMES_PULSE = /\bPULSE\b/; +// The stored type, or the series route's `kind === "pulse"` (its readers name +// the wire kind, not the type). +const NAMES_PULSE = /\bPULSE\b|===\s*"pulse"/; /** * A day folded from its hours' means with every hour weighing one: SQL @@ -113,7 +115,7 @@ const SQL_READERS: Record = { /** Files that name pulse and hold an in-memory mean or a mean-making fold. */ const TS_READERS: Record = { - "app/api/measurements/series/route.ts": HELPER, + "lib/measurements/series/read-raw.ts": HELPER, "lib/doctor-report/collect.ts": HELPER, "lib/doctor-report-pdf/clinical-summary.ts": HELPER, "lib/insights/comprehensive-generate.ts": HELPER, @@ -146,7 +148,7 @@ const TS_READERS: Record = { "lib/analytics/correlations-fast-path.ts": { exempt: "mood mean; pulse enters as the resting series, never a day mean", }, - "app/api/dashboard/summary/route.ts": { + "lib/dashboard/summary/cards.ts": { exempt: "places a card's seven-day sparkline mean against its band for the trend verdict; each sparkline point is already one rollup day value, so the mean weighs every day once", }, @@ -175,7 +177,7 @@ const TS_READERS: Record = { "app/api/insights/cards/route.ts": { exempt: "pulse is summarised from day means (readDayMeanSeries)", }, - "app/api/insights/comprehensive/route.ts": { + "lib/insights/comprehensive-overview/build.ts": { exempt: "summarises the mood series only", }, }; diff --git a/src/__tests__/resolved-field-consumer-guard.test.ts b/src/__tests__/resolved-field-consumer-guard.test.ts index e5cac26a3..6f7b21916 100644 --- a/src/__tests__/resolved-field-consumer-guard.test.ts +++ b/src/__tests__/resolved-field-consumer-guard.test.ts @@ -362,7 +362,7 @@ const V143_FIELDS: ResolvedFieldEntry[] = [ sameResolver: { pattern: VERDICT_RESOLVER, files: [ - "src/app/api/dashboard/summary/route.ts", + "src/lib/dashboard/summary/cards.ts", "src/lib/dashboard/snapshot.ts", ], }, @@ -410,7 +410,7 @@ const V143_FIELDS: ResolvedFieldEntry[] = [ sameResolver: { pattern: VERDICT_RESOLVER, files: [ - "src/app/api/measurements/series/route.ts", + "src/lib/measurements/series/read-series.ts", "src/lib/dashboard/snapshot.ts", ], }, diff --git a/src/__tests__/sharing-surface-guard.test.ts b/src/__tests__/sharing-surface-guard.test.ts index b30d2e7ee..e8e67147d 100644 --- a/src/__tests__/sharing-surface-guard.test.ts +++ b/src/__tests__/sharing-surface-guard.test.ts @@ -76,7 +76,7 @@ import type { ShareScope } from "@/lib/sharing/scope"; import { ADMITTED_MUTATING_HANDLERS } from "../../tests/fixtures/v137/sharing-matrix"; import { API_HANDLER, guardModuleOf } from "./helpers/auth-guards"; -import { readSourceText } from "./helpers/source-files"; +import { readSourceText, walkSourceFiles } from "./helpers/source-files"; const SRC = join(process.cwd(), "src"); @@ -435,6 +435,17 @@ function callsSymbol(rel: string, symbol: string): boolean { */ const HANDLER_BODY_MODULES: Record = { "app/api/medications/route.ts": ["lib/medications/create-medication.ts"], + "app/api/medications/[id]/route.ts": [ + "lib/medications/update-medication/update-medication.ts", + "lib/medications/delete-medication.ts", + ], + "app/api/medications/[id]/intake/route.ts": [ + "lib/medications/record-intake/record-intake.ts", + ], + "app/api/medications/intake/route.ts": [ + "lib/medications/update-intake-status/admission.ts", + "lib/medications/update-intake-status/update-intake-status.ts", + ], "app/api/medications/[id]/intake/[eventId]/route.ts": [ "lib/medications/update-intake-event.ts", "lib/medications/delete-intake-event.ts", @@ -3305,15 +3316,30 @@ describe("(g) the MANAGE route set is frozen", () => { // and refreshes no token. Credential policy belongs inside every resolver // and presence helper so a delegate or managed system job cannot select a // personal BYOK key or custom base URL first. + // The route hands its body to the overview modules, so the leg reads the + // route and every one of them: the probe is called among them, and no + // resolver is called by any. + const comprehensiveModules = [ + "app/api/insights/comprehensive/route.ts", + ...walkSourceFiles(join(SRC, "lib/insights/comprehensive-overview"), { + floor: 5, + }).map((f) => `lib/insights/comprehensive-overview/${f}`), + ]; + expect(read(comprehensiveModules[0])).toContain( + '"@/lib/insights/comprehensive-overview/overview"', + ); expect( - callsSymbol( - "app/api/insights/comprehensive/route.ts", - "probeProviderPresence", - ), + comprehensiveModules.some((m) => callsSymbol(m, "probeProviderPresence")), ).toBe(true); - expect( - callsSymbol("app/api/insights/comprehensive/route.ts", "resolveProvider"), - ).toBe(false); + for (const m of comprehensiveModules) { + for (const name of [ + "resolveProvider", + "resolveProviderChain", + "probeProviderChain", + ]) { + expect(callsSymbol(m, name), `${m} calls ${name}`).toBe(false); + } + } for (const name of [ "resolveProvider", "resolveProviderChain", diff --git a/src/__tests__/spread-single-implementation-guard.test.ts b/src/__tests__/spread-single-implementation-guard.test.ts index 434e49f7d..0ac981d2c 100644 --- a/src/__tests__/spread-single-implementation-guard.test.ts +++ b/src/__tests__/spread-single-implementation-guard.test.ts @@ -90,10 +90,7 @@ describe("one standard deviation", () => { it("the series API and the AI and MCP payload report the same spread", () => { expect( stripComments( - readSource( - "src/app/api/measurements/series/route.ts", - "READING_SPREAD", - ), + readSource("src/lib/measurements/series/kinds.ts", "READING_SPREAD"), ), ).toMatch(/standardDeviation\(\s*values,\s*READING_SPREAD\s*\)/); const features = stripComments( diff --git a/src/__tests__/tombstone-read-guard.test.ts b/src/__tests__/tombstone-read-guard.test.ts index ba32b1ca0..5977c3c41 100644 --- a/src/__tests__/tombstone-read-guard.test.ts +++ b/src/__tests__/tombstone-read-guard.test.ts @@ -53,6 +53,8 @@ const ALLOWED: Record = { "Withholds medication texts from the recall trail; a deleted fact still withholds, which errs toward saying less.", "coachFact@lib/ai/coach/memory/remember.ts": "Answer-once check on the message that proposed the fact, inside its lock.", + "moodEntry@lib/mood/bulk-ingest.ts": + "Write path, not analysis: resolves the row that won a raced external-id upsert, which owns the key whether or not it is deleted.", }; function stripComments(text: string): string { diff --git a/src/__tests__/unit-preference-display-guard.test.ts b/src/__tests__/unit-preference-display-guard.test.ts index a14dbbe91..79c26c6fd 100644 --- a/src/__tests__/unit-preference-display-guard.test.ts +++ b/src/__tests__/unit-preference-display-guard.test.ts @@ -253,6 +253,8 @@ const WIDE_SWEEP_ROOTS = [ "lib/jobs/", "lib/targets/", "lib/daily/", + // The series route's readers, moved out of `app/` with their unit tokens. + "lib/measurements/series/", ]; const UNIT_LITERAL_PATTERNS: readonly RegExp[] = [ @@ -359,7 +361,7 @@ const UNIT_LITERAL_ALLOWLIST: Record< lines: 1, reason: "the legacy export endpoint's canonical storage-unit contract", }, - "app/api/measurements/series/route.ts": { + "lib/measurements/series/kinds.ts": { lines: 4, reason: "canonical wire units; glucose is overridden to the reader's unit per request", @@ -512,7 +514,7 @@ const READER_UNIT_PRODUCERS = [ "lib/ai/prompts/weight.ts", "lib/insights/features-units.ts", "lib/insights/comprehensive-generate.ts", - "app/api/insights/generate/route.ts", + "lib/insights/briefing-request/features.ts", "lib/insights/weight-status.ts", "lib/insights/metric-status.ts", "lib/insights/narrative/period-narrative.ts", diff --git a/src/__tests__/workout-canonical-identity-guard.test.ts b/src/__tests__/workout-canonical-identity-guard.test.ts index 88d05da85..03b706acf 100644 --- a/src/__tests__/workout-canonical-identity-guard.test.ts +++ b/src/__tests__/workout-canonical-identity-guard.test.ts @@ -48,7 +48,8 @@ const PICKER_CALLERS = ["lib/workouts/canonical-read.ts"]; * acknowledgement. The value is the file that owns the read's `select`. */ const IDENTITY_OWNER: Record = { - "app/api/workouts/[id]/route.ts": "app/api/workouts/[id]/route.ts", + // The workout detail read behind `GET /api/workouts/{id}`. + "lib/workouts/detail.ts": "lib/workouts/detail.ts", "lib/jobs/workout-insight-generate.ts": "lib/jobs/workout-insight-generate.ts", "lib/workouts/list-read.ts": "lib/workouts/list-read.ts", diff --git a/src/app/__tests__/insights-b3-wiring.test.ts b/src/app/__tests__/insights-b3-wiring.test.ts index 08022ab9a..29585d8d2 100644 --- a/src/app/__tests__/insights-b3-wiring.test.ts +++ b/src/app/__tests__/insights-b3-wiring.test.ts @@ -21,7 +21,11 @@ import { join } from "node:path"; const ROOT = join(__dirname, "../../.."); const INSIGHTS_PATH = join(ROOT, "src/app/insights/page-client.tsx"); -const ANALYTICS_ROUTE_PATH = join(ROOT, "src/app/api/analytics/route.ts"); +/** The analytics default body, built outside its route. */ +const ANALYTICS_BODY_PATHS = [ + join(ROOT, "src/lib/analytics/response/default-body.ts"), + join(ROOT, "src/lib/analytics/response/correlations.ts"), +]; const ADVISOR_HOOK_PATH = join( ROOT, "src/components/insights/use-insights-advisor.ts", @@ -84,7 +88,7 @@ describe("v1.4.20 B3 — /api/analytics surfaces correlations", () => { }); it("returns a `correlations` block on the success payload", () => { - const src = load(ANALYTICS_ROUTE_PATH); + const src = ANALYTICS_BODY_PATHS.map(load).join("\n"); expect(src).toMatch(/correlations:\s*correlationsWithPatterns,/); expect(src).toMatch(/computeCorrelationHypotheses/); }); diff --git a/src/app/__tests__/insights-polish.test.ts b/src/app/__tests__/insights-polish.test.ts index 050614f17..258c3e767 100644 --- a/src/app/__tests__/insights-polish.test.ts +++ b/src/app/__tests__/insights-polish.test.ts @@ -238,8 +238,9 @@ describe("v1.4.22 W5 reconcile (Code-H2) — BD-Zielbereich tile compareDelta us }); it("ships priorMonth + priorYear pcts in the analytics envelope", () => { + // The default body is built outside the route. const ROUTE_SRC = readFileSync( - join(ROOT, "src/app/api/analytics/route.ts"), + join(ROOT, "src/lib/analytics/response/default-body.ts"), "utf8", ); expect(ROUTE_SRC).toContain("bpInTargetPctPriorMonth"); diff --git a/src/app/api/analytics/route.ts b/src/app/api/analytics/route.ts index 2cdc57ea3..41dedd7a6 100644 --- a/src/app/api/analytics/route.ts +++ b/src/app/api/analytics/route.ts @@ -1,46 +1,19 @@ -import { prisma } from "@/lib/db"; import { apiHandler, requireAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { apiSuccess, returnAllZodIssues } from "@/lib/api-response"; import { analyticsQuerySchema } from "@/lib/validations/analytics"; import { NO_STORE_BUT_BFCACHE } from "@/lib/http/cache-headers"; import { cachedSwr, caches, type ServerCache } from "@/lib/cache/server-cache"; -import { summarize, wireSummary, type DataPoint } from "@/lib/analytics/trends"; import { readSummariesSlice } from "@/lib/analytics/summaries-slice"; import { loadRecoveryEmpty } from "@/lib/insights/recovery-empty-read"; -import { resolveEffectiveBpTargets } from "@/lib/analytics/effective-range"; -import { bpTargetsEqual, getBpTargets } from "@/lib/analytics/bp-targets"; -import { DEFAULT_TIMEZONE } from "@/lib/tz/resolver"; -import { - buildSleepStageComposition, - type SleepStageComposition, -} from "@/lib/analytics/sleep-stage-composition"; -import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; -import { ensureUserRollupsFresh } from "@/lib/rollups/measurement-rollups"; -import { probeRollupCoverage } from "@/lib/rollups/measurement-coverage"; -import { - computeBpInTargetFastPath, - type BpInTargetEnvelope, -} from "@/lib/analytics/bp-in-target-fast-path"; -import { computeAndRecordUserHealthScore } from "@/lib/analytics/score/record"; -import { isModuleEnabled, resolveModuleMap } from "@/lib/modules/gate"; +import { resolveModuleMap } from "@/lib/modules/gate"; import { gateSummariesByModules } from "@/lib/dashboard/widget-modules"; -import { resolveRestMode } from "@/lib/illness/rest-mode"; -import { deriveBpWindow90 } from "@/lib/analytics/window-confidence"; -import { computeCorrelationHypothesesFastPath } from "@/lib/analytics/correlations-fast-path"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; -import { type Locale } from "@/lib/i18n/config"; import { - computeGlucoseClinicalMetrics, - GLUCOSE_PANEL_WINDOW_DAYS, - type GlucoseClinicalMetrics, -} from "@/lib/analytics/glucose-metrics"; -import { groupByGlucoseContext, resolveGlucoseUnit } from "@/lib/glucose"; -import { - decisionForEvidence, - PATTERN_FAMILIES, - syncAcceptedPatterns, -} from "@/lib/insights/correlation-patterns"; + buildAnalyticsResponse, + type AnalyticsBody, +} from "@/lib/analytics/response/default-body"; +import { enrichLastSeenDaysAgo } from "@/lib/analytics/response/last-seen"; export const dynamic = "force-dynamic"; @@ -154,9 +127,7 @@ export const GET = apiHandler(async (request?: Request) => { // cost a little duplication (most users have one locale). const locale = await resolveServerLocale({ userLocale: user.locale ?? null }); const cachedBody = await cachedSwr( - caches.analytics as ServerCache< - Awaited> - >, + caches.analytics as ServerCache, `${user.id}|default|${locale}`, () => buildAnalyticsResponse(user, locale), annotate, @@ -182,580 +153,3 @@ export const GET = apiHandler(async (request?: Request) => { response.headers.set("Cache-Control", NO_STORE_BUT_BFCACHE); return response; }); - -type AuthedUser = Awaited>["user"]; - -/** - * v1.4.38 — derive `daysAgo` from the cached `lastSeenAt` ISO string - * per request. The cached envelope deliberately omits `daysAgo` so - * the dashboard tile staleness caption stays correct across day - * boundaries even when the body has been sitting in the 60s LRU. - * - * Returns a shallow-copied envelope with the enriched - * `lastSeenByType` map; everything else is passed through unchanged. - */ -function enrichLastSeenDaysAgo< - T extends { - lastSeenByType: Record; - }, ->( - body: T, -): Omit & { - lastSeenByType: Record< - string, - { lastSeenAt: string; daysAgo: number } | null - >; -} { - const nowMs = Date.now(); - const enriched: Record< - string, - { lastSeenAt: string; daysAgo: number } | null - > = {}; - for (const [type, slot] of Object.entries(body.lastSeenByType)) { - if (slot === null) { - enriched[type] = null; - continue; - } - const lastMs = new Date(slot.lastSeenAt).getTime(); - const daysAgo = Math.floor((nowMs - lastMs) / (24 * 60 * 60 * 1000)); - enriched[type] = { lastSeenAt: slot.lastSeenAt, daysAgo }; - } - return { ...body, lastSeenByType: enriched }; -} - -/** - * v1.4.34 — the heavy default-slice body, lifted out of the route - * handler so `cached()` can wrap it. Returns the raw JSON payload; the - * route handler attaches `Cache-Control` headers afterward. The split - * matches the v1.4.33 snapshot LRU's `buildCoachSnapshot` → - * `buildCoachSnapshotImpl` shape. - */ -async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) { - // v1.4.37.1 — fire-and-forget the rollup-fresh check. - // Awaiting it on the read path can stall the Node event loop for - // 30–60 s on a power-user account whose iOS step samples keep the - // 90-day DAY window slightly stale, which starves /api/health, - // /api/version, and concurrent iOS calls. The downstream coverage - // probe falls back to live SQL when a type is uncovered, so - // correctness is preserved; the worst the user sees is data from - // up to ~60 s ago on the very first request after a fresh - // measurement lands, and the next request returns the up-to-date - // value once the background refresh completes. `ensureUserRollupsFresh` - // wraps its own try/catch, so the void call cannot reject. - void ensureUserRollupsFresh(user.id); - - // v1.4.37 — single per-type coverage probe shared by the - // bp_in_target / healthScore / correlations branches below. The - // probe is one indexed query against `measurement_rollups`; the - // three downstream branches each reuse the resulting `coverage` - // map to decide between the rollup-fast-path and the live - // fallback. Probing once instead of per-branch keeps the fan-out - // cost flat across the three branches. - const coverage = await probeRollupCoverage(user.id); - - // v1.4.25 — every day-bucket call inside this route honours the - // user's display timezone via `userDayKey(date, userTz)` (the legacy - // `berlinDayKey()` helper was retired in v1.4.40). - const userTz = user.timezone ?? DEFAULT_TIMEZONE; - - // v1.4.49.1 — replace the 15-way per-type `fetchMeasurementSeriesChunked` - // fan-out with the slim rollup-tier reader. The previous live walk - // against `measurements` (425-day window, ≥ 50 Prisma round-trips on a - // 15-type tenant) saturated the `p-limit(4)` slot pool for ~8 s on the - // production 467 k-row account — and because Prisma's 20-slot pool was - // simultaneously claimed by those slots, the concurrent - // `?slice=summaries` request (only ~9 small queries) queued behind it - // and observed the same ~8 s wall-clock. - // - // `computeSummariesSlice` reads `measurement_rollups` DAY buckets - // (sub-second) plus one 90-day narrow `$queryRaw` for the windowed - // avg / slope / r² columns, and falls back to a split-aggregate live - // path for users with incomplete rollup coverage. Response shape is - // identical to the previous fan-out output for every field the - // dashboard actually consumes: - // - `count / latest / min / max / mean` from DAY buckets - // - `avg7 / avg30 / slope7 / slope30` from the narrow query - // - `avg30LastMonth` from the same narrow query (added in this - // release; previously only the live walk produced it) - // - `avg30LastYear` populated for any type whose WMY tier carries - // the year-ago window - // - `lastSeenByType` from the per-type latest read - // - // `anomalyCount` and `slope90` left this wire in v1.37.19: the slim - // path could never compute the former (it always reported 0) and no - // client component read the latter — the insights pipeline computes - // both from its own `summarize()` / comprehensive-aggregator reads. - // The one field the slim path leaves at a default value is - // `avg30LastYear` on types with no WMY coverage, which matches the - // pre-fix behaviour for tenants whose year-ago window happened to - // fall outside the 425-day floor. - const slim = await readSummariesSlice(user.id); - // Same module gate as the slim slice and the snapshot builder — the - // default slice feeds the dashboard too (it is the thick half of the - // legacy two-cell path), so it cannot be the one feed that leaks a - // disabled module's types. - const thickModules = await resolveModuleMap(user.id); - const thickGated = gateSummariesByModules( - slim.summaries, - slim.lastSeenByType, - thickModules, - ); - const results = thickGated.summaries; - const lastSeenByType = thickGated.lastSeenByType; - - // `computeSummariesSlice` annotates `action: "analytics.get.slim"` - // for telemetry on the slim route. Restore the default-slice action - // name so the wide-event reflects the route the client actually - // called (otherwise every default-slice request would appear in the - // logs under the slim action). - annotate({ action: { name: "analytics.get" } }); - - // v1.4.23 — sleep-stage breakdown for the trailing 30 days. Only - // included when the user has stage-tagged rows in window; null - // otherwise so the UI can render a plain total without the - // breakdown card painting empty. - const sleepStages = await computeSleepStageBreakdown(user.id, userTz); - - // BMI calculation - let bmi: number | null = null; - if (user.heightCm && results.WEIGHT?.latest) { - const heightM = user.heightCm / 100; - bmi = Math.round((results.WEIGHT.latest / (heightM * heightM)) * 10) / 10; - } - - // BP in-target percentage (auto-calculated from date of birth) - let bpInTargetPct: number | null = null; - let bpInTargetPct7d: number | null = null; - let bpInTargetPct30d: number | null = null; - let bpInTargetPctAllTime: number | null = null; - /** - * v1.4.22 — period-aligned prior-window - * pcts so the BD-Zielbereich tile's comparison-overlay caption - * stops mismatching its math with its label. The tile's - * `compareDelta` is `last30Days - priorMonth` (or `… - priorYear`) - * matching the user's `comparisonBaseline` selection, never - * `last30Days - allTime` (the v1.4.22 A2 shortcut). - */ - let bpInTargetPctPriorMonth: number | null = null; - let bpInTargetPctPriorYear: number | null = null; - // v1.17 — 90-day pair count + effective label span for the tile's - // thin-data gate. Surfaced so the BD-Zielbereich tile renders "collecting - // data" until the window holds enough readings, with a dynamic span label. - let bpInTargetCount90: number | null = null; - let bpInTargetSpanDays90: number | null = null; - // v1.17 — hold the current + prior-week BP envelopes so the shared - // Health-Score input builder grades the pillar off the identical shape the - // dashboard snapshot uses (one builder, no per-surface assembly drift). - // The graded score + prior-week BP figures the score consumes are read off - // these envelopes inside `buildHealthScoreBpInputs`. - let bpEnvelope: BpInTargetEnvelope | null = null; - let bpEnvelopePriorWeek: BpInTargetEnvelope | null = null; - let bpEnvelopePriorTwoWeeks: BpInTargetEnvelope | null = null; - const bpTargets = resolveEffectiveBpTargets( - { - dateOfBirth: user.dateOfBirth, - gender: user.gender, - heightCm: user.heightCm ?? null, - }, - user.thresholdsJson, - ); - const clinicalBpTargets = getBpTargets(user.dateOfBirth); - if (bpTargets) { - const now = new Date(); - // v1.4.37 — probe-gated dispatcher replaces the inline chunked - // read. When BOTH BP types have DAY-bucket coverage the helper - // pairs the per-day MEAN SYS + MEAN DIA from `measurement_rollups` - // and counts in-target days against the five reporting windows, - // skipping the heavy 365-day chunked walk against `measurements`. - // Falls back to the legacy `computeBpInTargetWindows` over a - // chunked read when coverage is partial so a brand-new account - // (no buckets yet) still sees per-event numbers. The helper emits - // a `path: "rollup" | "live"` annotate so prod logs prove which - // branch fired. See `bp-in-target-fast-path.ts` for the - // documented per-day-mean approximation. - // - // v1.4.38 — two parallel runs: one anchored at `now` for the - // standard windows, one anchored at `now - 7d` so the Health-Score - // helper has a real prior-week BP pct to feed into its previous- - // window snapshot. The two probes share the coverage map and the - // same rollup / live branch decision; the extra call is one extra - // pair of `readRollupBuckets` reads on the rollup path and is a - // no-op when the user has no BP rows at all. - const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - const fourteenDaysAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000); - const [windows, windowsPriorWeek, windowsPriorTwoWeeks] = await Promise.all( - [ - computeBpInTargetFastPath({ - userId: user.id, - targets: bpTargets, - now, - coverage, - // v1.4.38 — cross-tz runtime guard. The helper falls back - // to the live SQL path when the user is more than 3 hours - // from UTC, where the rollup table's UTC-midnight day-key - // would slip a calendar day relative to the live aggregator's - // window cuts. - userTz, - }), - computeBpInTargetFastPath({ - userId: user.id, - targets: bpTargets, - now: sevenDaysAgo, - coverage, - userTz, - }), - computeBpInTargetFastPath({ - userId: user.id, - targets: bpTargets, - now: fourteenDaysAgo, - coverage, - userTz, - }), - ], - ); - // The pillar is graded against the CLINICAL band even when the user - // keeps a personal target (that band is the number's identity; the - // personal one is displayed beside it and scores nothing). So when the - // two differ we re-run the whole helper against the clinical targets - // and the score comes from that second run. - // - // v1.34.5 — the "why this number" basis rides the same envelope, so it - // is the same run, the same representative pair and the same ceilings - // by construction. Assign envelopes WHOLE here; picking `graded.score` - // off one run and a basis off the other would let the popover explain - // a number nobody is looking at, and it would look correct in review. - if (bpTargetsEqual(bpTargets, clinicalBpTargets)) { - bpEnvelope = windows; - bpEnvelopePriorWeek = windowsPriorWeek; - bpEnvelopePriorTwoWeeks = windowsPriorTwoWeeks; - } else if (clinicalBpTargets) { - [bpEnvelope, bpEnvelopePriorWeek, bpEnvelopePriorTwoWeeks] = - await Promise.all([ - computeBpInTargetFastPath({ - userId: user.id, - targets: clinicalBpTargets, - now, - coverage, - userTz, - }), - computeBpInTargetFastPath({ - userId: user.id, - targets: clinicalBpTargets, - now: sevenDaysAgo, - coverage, - userTz, - }), - computeBpInTargetFastPath({ - userId: user.id, - targets: clinicalBpTargets, - now: fourteenDaysAgo, - coverage, - userTz, - }), - ]); - } - // v1.17 — the BD-Zielbereich headline standardises on the - // trailing-90-day window (labelled "· 90 T" in the tile). All-time - // remains carried below for the BP detail page's long view only. - bpInTargetPct = windows.last90Days?.pct ?? null; - bpInTargetPct7d = windows.last7Days?.pct ?? null; - bpInTargetPct30d = windows.last30Days?.pct ?? null; - bpInTargetPctAllTime = windows.allTime?.pct ?? null; - bpInTargetPctPriorMonth = windows.priorMonth?.pct ?? null; - bpInTargetPctPriorYear = windows.priorYear?.pct ?? null; - // v1.17 — count + effective span for the BD-Zielbereich tile's - // confidence gate (thin-data → "collecting data"; dynamic span label). - // Shared helper keeps this identical to the dashboard snapshot. - const bpWindow = deriveBpWindow90( - windows.last90Days, - windows.last90EarliestAt, - now, - ); - bpInTargetCount90 = bpWindow.count; - bpInTargetSpanDays90 = bpWindow.spanDays; - } - - // Per-context glucose summaries (canonical mg/dL). - // - // v1.4.29 — bound to the trailing 30 days. The dashboard tile - // path only reads the trailing window; pre-bound this query - // walked every persisted BG row a multi-year user has written. - const glucoseSince = new Date( - Date.now() - GLUCOSE_PANEL_WINDOW_DAYS * 24 * 60 * 60 * 1000, - ); - const glucoseRows = await prisma.measurement.findMany({ - where: { - userId: user.id, - type: "BLOOD_GLUCOSE", - measuredAt: { gte: glucoseSince }, - deletedAt: null, - }, - orderBy: { measuredAt: "asc" }, - select: { value: true, measuredAt: true, glucoseContext: true }, - }); - // #943 — grouped through the shared bucket resolver, so readings with no - // meal-time tag land in `UNSPECIFIED` instead of being dropped on the floor. - const glucoseByContext: Record> = {}; - for (const [bucket, ctxRows] of groupByGlucoseContext( - glucoseRows, - (r) => r.glucoseContext, - )) { - glucoseByContext[bucket] = wireSummary( - summarize( - ctxRows.map((r): DataPoint => ({ date: r.measuredAt, value: r.value })), - ), - ); - } - - // v1.17.0 — server-authoritative glucose clinical panel. The TIR / GMI / - // eA1C / CV% headline and the advanced J-index + LBGI/HBGI tier are computed - // ONCE here over the declared 30-day window (matching `glucoseSince`) so the - // insights panel, the coach snapshot, and the doctor report all render the - // same numbers instead of each re-deriving. The learning gate keeps a thin - // spot-data window from being asserted as a clinical AGP. Always present in - // the payload (even with zero readings) so the client renders the calm - // "still learning" state from a populated object rather than a missing key. - const glucoseClinical: GlucoseClinicalMetrics = computeGlucoseClinicalMetrics( - glucoseRows.map((r) => ({ measuredAt: r.measuredAt, mgdl: r.value })), - { windowDays: GLUCOSE_PANEL_WINDOW_DAYS, now: new Date() }, - ); - - // v1.4.20 — three pre-defined correlation hypotheses. - // v1.4.37 — probe-gated helper. The 28-day scan window (down - // from 30 to keep the cold critical path tight while still - // satisfying the n >= 20 surface gate) reads SYS / PULSE / WEIGHT - // per-day means from `measurement_rollups` when the user has full - // coverage; mood and medication-intake reads always stay live (no - // rollup equivalent). The helper emits `meta.correlations.path` + - // `meta.correlations.window_days` so prod logs prove the branch - // selection and the truthful window. - const correlations = await computeCorrelationHypothesesFastPath({ - userId: user.id, - userTz, - now: new Date(), - coverage, - locale, - }); - const fixedDefinitions = [ - { - key: "bpCompliance" as const, - factorKey: "HYPOTHESIS:MEDICATION_COMPLIANCE", - outcomeKey: "BLOOD_PRESSURE_SYS", - }, - { - key: "moodPulse" as const, - factorKey: "HYPOTHESIS:MOOD", - outcomeKey: "RESTING_PULSE", - }, - { - key: "weightWeekday" as const, - factorKey: "HYPOTHESIS:WEEKDAY", - outcomeKey: "WEIGHT", - }, - ]; - const acceptedFixed = fixedDefinitions.flatMap((definition) => { - const result = correlations[definition.key]; - return result.status === "ok" - ? [ - { - factorKey: definition.factorKey, - outcomeKey: definition.outcomeKey, - lagDays: 0, - sampleSize: result.n, - effectSize: result.statistic, - pValue: result.pValue, - qValue: null, - }, - ] - : []; - }); - const fixedDecisions = await syncAcceptedPatterns({ - userId: user.id, - family: PATTERN_FAMILIES.fixed, - accepted: acceptedFixed, - }); - const correlationsWithPatterns = { ...correlations }; - for (const definition of fixedDefinitions) { - const result = correlations[definition.key]; - if (result.status !== "ok") continue; - const decision = decisionForEvidence(fixedDecisions, { - factorKey: definition.factorKey, - outcomeKey: definition.outcomeKey, - lagDays: 0, - }); - correlationsWithPatterns[definition.key] = decision - ? { ...result, ...decision } - : result; - } - - // v1.34 — the versioned cardiometabolic reference report. The shared reader - // admits only evidence-backed pillars with their own windows and coverage - // floors, then applies the three-domain breadth rule. Its BP envelopes come - // from the same rollup/live fast path the dashboard uses; every other domain - // resolves through the canonical reader and fails independently. - const [ - glucoseEnabled, - labsEnabled, - sleepEnabled, - mentalHealthEnabled, - scoreSourcePriority, - ] = await Promise.all([ - isModuleEnabled(user.id, "glucose"), - isModuleEnabled(user.id, "labs"), - isModuleEnabled(user.id, "sleep"), - isModuleEnabled(user.id, "mentalHealth"), - loadUserSourcePriority(user.id), - ]); - const glucoseByContextOut = glucoseEnabled ? glucoseByContext : null; - const glucoseClinicalOut = glucoseEnabled ? glucoseClinical : null; - const scoredAt = new Date(); - const healthScore = await computeAndRecordUserHealthScore({ - userId: user.id, - now: scoredAt, - profile: { - dateOfBirth: user.dateOfBirth, - heightCm: user.heightCm ?? null, - timezone: userTz, - sourcePriorityJson: scoreSourcePriority, - thresholdsJson: user.thresholdsJson ?? null, - glucoseUnit: resolveGlucoseUnit(user.glucoseUnit), - }, - modules: { - glucose: glucoseEnabled, - labs: labsEnabled, - sleep: sleepEnabled, - mentalHealth: mentalHealthEnabled, - }, - healthScoreConfigJson: user.healthScoreConfigJson, - bpTargets: clinicalBpTargets, - bpEnvelope, - bpEnvelopePriorWeek, - bpEnvelopePriorTwoWeeks, - }); - - // v1.18.1 — Rest Mode annotation. When an illness/condition episode is - // active we frame the score (not penalise it): the number above is left - // exactly as computed and we attach a value-free "you were unwell" context. - // Resolved server-side so iOS mirrors the same fact, never recomputing it. - // Resolved as-of the scored window (`scoredAt`), not a fresh now. - if (healthScore) { - const restMode = await resolveRestMode(user.id, scoredAt); - healthScore.restMode = restMode.active - ? { - active: true, - since: restMode.since, - episodeCount: restMode.episodeCount, - } - : null; - } - - return { - summaries: results, - bmi, - bpInTargetPct, - bpInTargetPct7d, - bpInTargetPct30d, - bpInTargetPctAllTime, - bpInTargetPctPriorMonth, - bpInTargetPctPriorYear, - bpInTargetCount90, - bpInTargetSpanDays90, - glucoseByContext: glucoseByContextOut, - glucoseClinical: glucoseClinicalOut, - correlations: correlationsWithPatterns, - healthScore, - sleepStages, - // v1.28.x — the latest night's source-discrepancy annotation, computed - // inside the slim slice's per-night post-pass. Additive: the dashboard - // sleep tile marks the headline with a discreet "sources disagree" - // hint; clients that don't read the field stay unchanged. - sleepSourceDiscrepancy: slim.sleepSourceDiscrepancy, - // v1.4.34 — per-type freshness map drives the dashboard's - // staleness caption on each ``. Additive: clients that - // don't read the field stay unchanged. - lastSeenByType, - }; -} - -/** - * Per-stage sleep-minutes breakdown over the trailing 30 days. - * - * Returns `null` when the user has no stage-tagged sleep rows in - * window — the analytics consumer renders the existing - * `summaries.SLEEP_DURATION` totals without a stage card in that - * case. Returns the sum-per-stage AND the day count covered so the - * UI can render an "averaged across N nights" caption truthfully. - * - * v1.4.25 — also returns a `perNight` array: one entry per - * Berlin-tz day inside the window, with minutes-per-stage for the - * stacked-bar chart. Days with zero stage-tagged rows are omitted - * from `perNight` (the chart treats them as gaps). - * - * The per-night stages are the MAIN NIGHT's stages. A nap ends on the - * same wake day as that morning's night, and the per-wake-day totals - * folded it into the night's buckets — an afternoon nap added CORE to - * the night and the column read longer and shallower than the night - * was. The nap's minutes now ride alongside on `napMinutes` / - * `napCount`, both absent on a day without one. - */ -async function computeSleepStageBreakdown( - userId: string, - userTz: string, -): Promise { - const DAY_MS = 24 * 60 * 60 * 1000; - const WINDOW_DAYS = 30; - const since = new Date(Date.now() - WINDOW_DAYS * DAY_MS); - // v1.11.5 — read the FULL per-stage row set (not `sleepStage: { not: null }`) - // and route it through the canonical session reconstruction so the - // stacked-bar agrees with the dashboard tile + chart series. The old path - // keyed each stage row by its OWN calendar day (splitting a midnight- - // spanning night across two buckets) and never collapsed multi-source - // (double-counting a WHOOP + Apple Health night). Routing through the - // helper fixes both: it session-clusters, keys by the wake day, and picks - // one canonical source per night. The bare-vs-granular de-dup is applied - // per-session to the per-stage breakdown too. The `source` is selected so - // the helper's source-collapse can run. - const rows = await prisma.measurement.findMany({ - where: { - userId, - type: "SLEEP_DURATION", - measuredAt: { gte: since }, - deletedAt: null, - }, - select: { - value: true, - measuredAt: true, - sleepStage: true, - source: true, - deviceType: true, - }, - }); - - if (rows.length === 0) return null; - - const priorityJson = await loadUserSourcePriority(userId); - return buildSleepStageComposition(rows, userTz, priorityJson, WINDOW_DAYS); -} - -// v1.4.37 — `computeCorrelationHypotheses` body relocated to -// `src/lib/analytics/correlations-fast-path.ts` so the probe-gated -// rollup / live dispatcher can be unit-tested independently of the -// route. The new helper also tightens the scan window to 28 days and -// emits a sentinel on `meta.correlations.window_days` / `degraded`. - -// v1.4.49.1 — the chunked `fetchMeasurementSeriesChunked` helper and -// its supporting types / constants relocated entirely. The default -// slice now delegates the per-type summaries work to -// `computeSummariesSlice` (rollup-tier, sub-second on covered tenants) -// so the legacy 15-way live walk against `measurements` no longer has -// a caller. The slim slice was already independent of it. Deleting the -// dead code here removes the pool-starvation surface that gated the -// concurrent slim slice and prevents future regressions from -// accidentally reintroducing the fan-out. - -// v1.4.37 — `WEEKDAY_FORMATTER_CACHE`, `getWeekdayFormatter`, -// `dateFromDayKey`, `ISO_WEEKDAY`, and `isoWeekdayInTz` relocated -// alongside the correlation runner in -// `src/lib/analytics/correlations-fast-path.ts`. They were only ever -// used by the weight-weekday + day-key helpers inside the old inline -// correlation builder. diff --git a/src/app/api/auth/oidc/callback/route.ts b/src/app/api/auth/oidc/callback/route.ts index af57ea28a..fc7a1f6d3 100644 --- a/src/app/api/auth/oidc/callback/route.ts +++ b/src/app/api/auth/oidc/callback/route.ts @@ -3,17 +3,11 @@ import { NextRequest, NextResponse } from "next/server"; import { apiHandler } from "@/lib/api-handler"; import { annotate, getEvent } from "@/lib/logging/context"; import { checkAuthSurfaceRateLimit } from "@/lib/rate-limit"; -import { decrypt } from "@/lib/crypto"; -import { prisma } from "@/lib/db"; -import { auditLog } from "@/lib/auth/audit"; import { - createSession, validateSessionFromCookieValue, SESSION_COOKIE_NAME, } from "@/lib/auth/session"; -import { recordSignInDevice } from "@/lib/auth/login-alert"; import { - deriveUniqueUsername, discoverOidcMetadata, exchangeCodeForTokens, fetchUserinfoEmail, @@ -29,15 +23,19 @@ import { OIDC_STATE_COOKIE, OIDC_STATE_COOKIE_PATH, } from "@/lib/auth/oidc-cookie"; -import { createMfaChallenge } from "@/lib/auth/mfa/challenge"; -import { syncMfaEnrollCookie } from "@/lib/auth/mfa-enrollment"; import { shouldEmitSecureCookie } from "@/lib/auth/secure-cookie"; -import { resolveServerDefaultTimezone } from "@/lib/tz/resolver"; -import { - buildNativeCallbackUrl, - mintNativeHandoff, -} from "@/lib/auth/oidc-native-handoff"; +import { buildNativeCallbackUrl } from "@/lib/auth/oidc-native-handoff"; import { setPendingLinkCookie } from "@/lib/auth/oidc-pending-link"; +import { + idpErrorCode, + readStoredOidcState, +} from "@/lib/auth/oidc-callback/state"; +import { resolveOidcAccount } from "@/lib/auth/oidc-callback/resolve-account"; +import { + mintOidcNativeHandoff, + openOidcSession, + startOidcSecondFactor, +} from "@/lib/auth/oidc-callback/complete-sign-in"; import { CLAIM_COOKIE, CLAIM_ERROR_PATH, @@ -74,84 +72,6 @@ function deleteStateCookie(response: NextResponse): void { }); } -/** - * Closed set of RFC 6749 §4.1.2.1 / OIDC Core §3.1.2.6 authorization error - * codes. The wide-event meta only ever carries a member of this set (or - * "other") — never the raw, attacker-influenceable query value. - */ -const KNOWN_IDP_ERROR_CODES = new Set([ - "access_denied", - "invalid_request", - "unauthorized_client", - "unsupported_response_type", - "invalid_scope", - "server_error", - "temporarily_unavailable", - "interaction_required", - "login_required", - "account_selection_required", - "consent_required", - "invalid_request_uri", - "invalid_request_object", - "request_not_supported", - "request_uri_not_supported", - "registration_not_supported", -]); - -interface StoredOidcState { - state: string; - nonce: string; - codeVerifier: string; - next: string; - /** - * v1.30.x — set only when the login GET carried `client=native`. When true - * the callback mints a handoff code (or MFA ticket) and redirects to the - * custom scheme instead of minting a session. Both fields live ONLY inside - * the AES-256-GCM blob, so they are tamper-authenticated: a network attacker - * or the IdP cannot flip a web login into a native one or swap the app - * challenge. - */ - native?: boolean; - /** The app's S256 PKCE challenge, bound at login; verified at the exchange. */ - appCodeChallenge?: string; - /** - * v1.43 (#959) — set only when the login carried `intent=claim` and a valid - * `hl_claim` cookie. The callback then claims the managed profile the - * cookie names instead of signing in, and only when the cookie still - * carries this exact binding. Web only: never together with `native`. - */ - intent?: "claim"; - claimBinding?: string; -} - -function isStoredOidcState(value: unknown): value is StoredOidcState { - const v = value as Record; - if ( - typeof value !== "object" || - value === null || - typeof v.state !== "string" || - typeof v.nonce !== "string" || - typeof v.codeVerifier !== "string" || - typeof v.next !== "string" - ) { - return false; - } - // Optional native fields: when present they must be the right shape, and a - // native start MUST carry the challenge the exchange verifies against. - if (v.native !== undefined) { - if (v.native !== true) return false; - if (typeof v.appCodeChallenge !== "string") return false; - } - if (v.intent !== undefined) { - if (v.intent !== "claim") return false; - if (typeof v.claimBinding !== "string" || v.claimBinding.length === 0) { - return false; - } - if (v.native !== undefined) return false; - } - return true; -} - /** * OIDC callback. Must work with NO existing session — this route * authenticates the user in the first place, unlike the per-account @@ -159,6 +79,9 @@ function isStoredOidcState(value: unknown): value is StoredOidcState { * `requireAuth()`. Every failure branch redirects to `/auth/login?error=...` * (never JSON — this is a top-level browser navigation) and deletes the * single-use state cookie, mirroring `src/app/api/withings/callback/route.ts`. + * + * The state blob, the account resolution and the three endings (second + * factor, native handoff, session) live in `src/lib/auth/oidc-callback/`. */ export const GET = apiHandler(async (req: NextRequest) => { annotate({ action: { name: "auth.oidc.callback" } }); @@ -169,16 +92,7 @@ export const GET = apiHandler(async (req: NextRequest) => { // the web login page — the app parses `?error=` and never lands on a // dead web page. A missing/undecryptable blob leaves `stored` null (isNative // false) and those branches fall back to the web redirect by construction. - const rawCookie = req.cookies.get(OIDC_STATE_COOKIE)?.value; - let stored: StoredOidcState | null = null; - if (rawCookie) { - try { - const parsed: unknown = JSON.parse(decrypt(rawCookie)); - if (isStoredOidcState(parsed)) stored = parsed; - } catch { - stored = null; - } - } + const stored = readStoredOidcState(req.cookies.get(OIDC_STATE_COOKIE)?.value); const isNative = stored?.native === true; const isClaim = stored?.intent === "claim"; @@ -229,7 +143,7 @@ export const GET = apiHandler(async (req: NextRequest) => { annotate({ meta: { reason: "idp_denied", - idpError: KNOWN_IDP_ERROR_CODES.has(idpError) ? idpError : "other", + idpError: idpErrorCode(idpError), }, }); return failRedirect("oidc_denied"); @@ -367,198 +281,32 @@ export const GET = apiHandler(async (req: NextRequest) => { return response; } - // (1) Durable identity match — the (issuer, sub) pair stamped on a - // previous login, link, or provision. Email is deliberately NOT - // consulted here: after the stamp it is a display field, so an - // IdP-side email change can refresh it below but can never re-point - // the login at a different account. - let user = await prisma.user.findFirst({ - where: { oidcIssuer: metadata.issuer, oidcSub: identity.sub }, + const account = await resolveOidcAccount({ + issuer: metadata.issuer, + sub: identity.sub, + email, + emailVerified, + ip, }); - - if (user) { - // Refresh the display email only — and only from a claim the IdP - // explicitly marks verified. Skipped when another account already - // holds the address (`email` is unique); the login itself proceeds - // either way, keyed by (issuer, sub). - const displayEmail = email ? email.toLowerCase() : null; - if ( - displayEmail && - emailVerified === true && - user.email?.toLowerCase() !== displayEmail - ) { - const emailTaken = await prisma.user.findFirst({ - where: { - email: { equals: displayEmail, mode: "insensitive" }, - NOT: { id: user.id }, - }, - select: { id: true }, - }); - if (!emailTaken) { - user = await prisma.user.update({ - where: { id: user.id }, - data: { email: displayEmail }, - }); - } + if (account.kind === "refused") return failRedirect(account.reason); + if (account.kind === "linkPending") { + if (isNative) { + // The native sign-in sheet cannot run the confirmation; the person + // signs in on the web once, after which the app's SSO works. + return failRedirect("oidc_link_required"); } - } else { - // No stamped identity yet — both remaining paths (link-once, - // provision) bind this (issuer, sub) to an account keyed by email, - // so the claim must be explicitly `email_verified: true`. An ABSENT - // claim is a reject, not a benefit of the doubt: an IdP that does - // not assert verification cannot anchor a link that later gates a - // health record. - if (!email) { - annotate({ meta: { reason: "no_email" } }); - return failRedirect("oidc_no_email"); - } - if (emailVerified !== true) { - annotate({ meta: { reason: "email_unverified" } }); - return failRedirect("oidc_email_unverified"); - } - - const byEmail = await prisma.user.findFirst({ - where: { email: { equals: email, mode: "insensitive" } }, + const response = errorRedirect("oidc_link_required"); + setPendingLinkCookie(response, { + userId: account.userId, + issuer: metadata.issuer, + sub: identity.sub, }); - - if (byEmail) { - // (2) Link ONCE. An account already pinned to a different IdP - // identity is never silently re-bound — a rebind would let a - // second IdP identity that acquires the same email capture the - // account. Audited so the operator can see the collision. - if (byEmail.oidcIssuer !== null || byEmail.oidcSub !== null) { - await auditLog("auth.oidc.link_conflict", { - userId: byEmail.id, - ipAddress: ip, - }); - annotate({ meta: { reason: "identity_conflict" } }); - return failRedirect("oidc_identity_conflict"); - } - // (2b) Never link on the email alone. The local address was never - // verified — anyone can register with it or type it into a profile — - // so matching it proves nothing about who owns the account. Record - // the link this login would make and send the browser to sign in to - // the account with its own credential; the link is made there - // (`completePendingOidcLink`), and only for this account. - await auditLog("auth.oidc.link_pending", { - userId: byEmail.id, - ipAddress: ip, - }); - annotate({ meta: { reason: "link_confirmation_required" } }); - if (isNative) { - // The native sign-in sheet cannot run the confirmation; the person - // signs in on the web once, after which the app's SSO works. - return failRedirect("oidc_link_required"); - } - const response = errorRedirect("oidc_link_required"); - setPendingLinkCookie(response, { - userId: byEmail.id, - issuer: metadata.issuer, - sub: identity.sub, - }); - return response; - } else { - // (3) Provision. - const userCount = await prisma.user.count(); - let registrationEnabled = true; - try { - const settings = await prisma.appSettings.findUnique({ - where: { id: "singleton" }, - }); - if (settings && !settings.registrationEnabled && userCount > 0) { - registrationEnabled = false; - } - } catch { - // Table may not exist yet; allow provisioning (matches register/route.ts). - } - if (!registrationEnabled) { - annotate({ meta: { reason: "registration_disabled" } }); - return failRedirect("oidc_registration_disabled"); - } - - const username = await deriveUniqueUsername(email, (candidate) => - prisma.user - .findUnique({ where: { username: candidate } }) - .then((u: unknown) => u !== null), - ); - - // Same server-default resolution the register route uses — an - // SSO-provisioned account has no registration form to carry the - // browser-detected timezone. - const timezone = await resolveServerDefaultTimezone(); - - // v1.28.48 — the first provisioned user becomes ADMIN. Deriving that - // role from the early `userCount` (read above, before username - // derivation) and then creating without coordination is the same - // check-then-act race the password-register route closed in v1.28.42: - // two concurrent OIDC first-logins racing the empty-DB window would - // both observe `0` and both be minted ADMIN. Serialise the - // count+insert behind a transaction-scoped advisory lock (released on - // commit/rollback) and re-count *inside* the lock so the second - // provision observes the first's committed row and is minted USER. - // The lock key STRING matches the register route's — an OIDC - // first-login racing a password first-registration is serialised - // against the same lock, so only one ADMIN can ever be minted. - user = await prisma.$transaction(async (tx) => { - // `pg_advisory_xact_lock` returns void, which the client cannot - // deserialize as a column — selecting FROM it yields a plain int row. - await tx.$queryRaw` - SELECT 1 AS locked - FROM pg_advisory_xact_lock(hashtextextended('register:first-admin', 0)) - `; - const priorUsers = await tx.user.count(); - const role = priorUsers === 0 ? "ADMIN" : "USER"; - return tx.user.create({ - data: { - email: email.toLowerCase(), - username, - passwordHash: null, - role, - timezone, - oidcIssuer: metadata.issuer, - oidcSub: identity.sub, - }, - }); - }); - - await auditLog("auth.oidc.provisioned", { - userId: user.id, - ipAddress: ip, - }); - } + return response; } + const user = account.user; - // Native second factor. OIDC is a DELEGATED factor: it proves the IdP - // authenticated someone, but it never substitutes for this app's own - // second factor. An account with a confirmed TOTP secret or a - // registered security key gets the same MFA challenge step password - // login gets — no session is minted here; the single-use ticket rides - // the handoff cookie to the login page (web) or the custom scheme - // (native), which render the same challenge UI and complete at - // `/api/auth/mfa/verify`. (The trusted-device skip cannot apply on this - // path: its cookie is SameSite=Strict, which the browser withholds on an - // IdP-initiated redirect chain.) - const hasTotp = Boolean(user.totpConfirmedAt); - const webauthnKeyCount = await prisma.webauthnMfaCredential.count({ - where: { userId: user.id }, - }); - const hasWebauthn = webauthnKeyCount > 0; - - if (hasTotp || hasWebauthn) { - const challenge = await createMfaChallenge(user.id, "login"); - const methods: ("totp" | "recovery" | "webauthn")[] = []; - if (hasTotp) methods.push("totp", "recovery"); - if (hasWebauthn) methods.push("webauthn"); - await auditLog("auth.mfa.challenge", { - userId: user.id, - ipAddress: ip, - details: { source: isNative ? "login.oidc.native" : "login.oidc" }, - }); - annotate({ - action: { name: "auth.mfa.challenge" }, - meta: { mfa_required: true }, - }); - + const secondFactor = await startOidcSecondFactor(user, ip, isNative); + if (secondFactor) { // Native: hand the app the SAME single-use ticket via the custom scheme. // No `OIDC_MFA_COOKIE` (it exists only to ferry the ticket to the web // login page's script) and no session — the app completes at the @@ -569,8 +317,8 @@ export const GET = apiHandler(async (req: NextRequest) => { if (isNative) { const response = NextResponse.redirect( buildNativeCallbackUrl({ - mfa_ticket: challenge.ticket, - methods: methods.join(","), + mfa_ticket: secondFactor.ticket, + methods: secondFactor.methods.join(","), }), ); deleteStateCookie(response); @@ -585,7 +333,10 @@ export const GET = apiHandler(async (req: NextRequest) => { deleteStateCookie(response); response.cookies.set( OIDC_MFA_COOKIE, - JSON.stringify({ ticket: challenge.ticket, methods }), + JSON.stringify({ + ticket: secondFactor.ticket, + methods: secondFactor.methods, + }), { // Not httpOnly by design — see the constant's doc comment. httpOnly: false, @@ -600,26 +351,14 @@ export const GET = apiHandler(async (req: NextRequest) => { const ua = req.headers.get("user-agent"); - // Native, no MFA: mint a one-time handoff code and hand it to the app via - // the custom scheme. NO session and NO cookie are minted here — the app - // exchanges the code at `POST /api/auth/oidc/native/token` for the native - // bundle. `userId` is the resolved identity; the app's S256 challenge - // (bound at login, tamper-authenticated in the state blob) locks the code - // to the app instance that started the flow. The session/device are - // recorded at the exchange via `finishLogin`, not here. if (isNative) { - const { code: handoffCode } = await mintNativeHandoff({ + const handoffCode = await mintOidcNativeHandoff({ userId: user.id, - // Guaranteed present by `isStoredOidcState` when `native === true`. + // Guaranteed present by the state reader when `native === true`. appCodeChallenge: stored.appCodeChallenge as string, - ipAddress: ip, + ip, userAgent: ua, }); - await auditLog("auth.oidc.native.handoff_minted", { - userId: user.id, - ipAddress: ip, - }); - annotate({ action: { name: "auth.oidc.native.handoff_minted" } }); const response = NextResponse.redirect( buildNativeCallbackUrl({ code: handoffCode }), ); @@ -627,28 +366,7 @@ export const GET = apiHandler(async (req: NextRequest) => { return response; } - // v1.23 parity with password login — the admin-enforced-MFA hint cookie - // sends a single-factor account into forced enrollment after sign-in. - await syncMfaEnrollCookie(user.id, { - totpConfirmedAt: user.totpConfirmedAt, - mfaEnforced: user.mfaEnforced, - }); - - // The 5th argument stays null: OIDC is a delegated factor, so an SSO - // session must never satisfy `requireFreshMfa` step-up (MFA disable, - // encryption-key rotation, encrypted export, account deletion) — the - // same treatment a trusted-device login gets. - await createSession( - user.id, - user.onboardingCompletedAt === null, - ip, - ua, - null, - ); - - void recordSignInDevice({ userId: user.id, ip, userAgent: ua }); - - await auditLog("auth.oidc.login", { userId: user.id, ipAddress: ip }); + await openOidcSession(user, ip, ua); const response = NextResponse.redirect(oidcAppUrl(stored.next)); deleteStateCookie(response); diff --git a/src/app/api/dashboard/summary/route.ts b/src/app/api/dashboard/summary/route.ts index 4526bf946..1603472de 100644 --- a/src/app/api/dashboard/summary/route.ts +++ b/src/app/api/dashboard/summary/route.ts @@ -52,284 +52,21 @@ * belongs to a disabled module never ships. The decision comes from the same * map the shared snapshot builder uses (`disabledSummaryTypes`); the filter * runs OUTSIDE the cache because a module toggle does not evict it. + * + * The builder lives in `src/lib/dashboard/summary/`. */ -import { loadFreeWeekdaySpans } from "@/lib/profile/life-circumstances"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { apiSuccess } from "@/lib/api-response"; import { annotate } from "@/lib/logging/context"; -import { prisma } from "@/lib/db"; -import type { MeasurementType } from "@/generated/prisma/client"; -import { measurementTypeEnum } from "@/lib/validations/measurement"; -import { CUMULATIVE_HK_TYPES } from "@/lib/measurements/apple-health-mapping"; -import { getServerTranslator } from "@/lib/i18n/server-translator"; -import { - resolveTrendVerdict, - trendSentimentOfTrend, - type TrendSentiment, - type TrendVerdict, - type TrendVerdictContext, -} from "@/lib/targets/trend-verdict"; -import { type Locale } from "@/lib/i18n/config"; -import { resolveJobLocale } from "@/lib/i18n/job-locale"; -import { userDayKey, DEFAULT_TIMEZONE } from "@/lib/tz/resolver"; +import { DEFAULT_TIMEZONE } from "@/lib/tz/resolver"; import { validTimezoneOr } from "@/lib/tz/format"; -import { startOfLocalDayKey } from "@/lib/tz/local-day"; import { cachedSwr, caches, type ServerCache } from "@/lib/cache/server-cache"; -import { buildMedsTodayBlock } from "@/lib/dashboard/meds-today"; -import { computeStreak } from "@/lib/dashboard/streak"; -import { - readActivityDayKeys, - readAllTimeCounts, - readLatestEver, - readSparkBuckets, -} from "@/lib/dashboard/summary-reads"; import { resolveModuleMap } from "@/lib/modules/gate"; import { gateMetricCardsByModules } from "@/lib/dashboard/widget-modules"; import { - summarizeSleepNights, - reconstructSleepNights, - type SleepSourceDiscrepancy, - type SleepStageRow, -} from "@/lib/analytics/sleep-night"; -import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; -import type { SleepStage } from "@/generated/prisma/client"; -import { loadBaselineProfile } from "@/lib/insights/derived/baseline"; -import { computeBmi } from "@/lib/insights/derived/bmi"; -import { isDerivedOk } from "@/lib/insights/derived/types"; -import { buildMoodDailySeries } from "@/lib/analytics/mood-series"; -import { - reconstructNights, - sleepNeedMinutes, -} from "@/lib/insights/derived/sleep-score"; -import { - computeSleepRhythmFromNights, - type SleepRhythmDto, -} from "@/lib/insights/derived/sleep-rhythm"; - -const SPARK_DAYS = 7; -const STREAK_WINDOW_DAYS = 365; - -type MetricKind = - | "weight" - | "bloodPressure" - | "pulse" - | "bodyFat" - | "glucose" - | "sleep" - | "steps" - | "totalBodyWater" - | "boneMass" - | "oxygenSaturation" - | "mood" - | "bmi"; - -interface MetricCard { - id: string; - kind: MetricKind; - /** - * v1.5.x — i18n key for the metric's display title (e.g. - * `dashboard.metric.title.weight`). Clients resolve the key against - * their own locale bundle: the PWA via `useTranslations()`, the iOS - * client via its bundled `Localizable.xcstrings`. The previous wire - * shape carried a German string literal which injected DE into an - * English-locale iOS UI. - */ - titleKey: string; - latestValue: number | null; - secondaryValue: number | null; - /** - * v1.5.x — i18n key for the metric's display unit (e.g. - * `dashboard.metric.unit.weight`). Same resolution contract as - * `titleKey`. SI tokens like `kg`, `mmHg`, `bpm`, `mg/dL` are - * identical across every shipped locale; per-kind keys keep the - * door open for locale-specific overrides (e.g. Imperial units) - * without another wire-shape change. - */ - unitKey: string; - /** - * v1.11.4 — explicit unit token for clients that need to know the - * value's unit without inferring it from the metric kind. Most tiles - * leave this `null` (the `unitKey` i18n key already carries the - * display unit). The `sleep` tile sets it to `"h"` because its - * `latestValue` is a per-NIGHT total expressed in HOURS (a float), - * not the canonical `SLEEP_DURATION` minutes — see the sleep block - * below for why the night total replaced the single-stage value. - */ - unit: string | null; - /** - * v1.11.4 — per-stage minutes for the headline night, sleep tile only. - * `null` for every other kind and for a sleep night with no - * stage-tagged rows (a legacy bare-duration night). Additive: a future - * sleep detail view can render the breakdown without changing the - * headline `latestValue`. - */ - sleepStages: Partial> | null; - /** - * v1.28.x — source-discrepancy annotation for the headline night's MAIN - * session, sleep tile only; `null` for every other kind and for a night - * whose writers agree. Additive (the established wire pattern for the - * iOS-locked shape): observational only — `latestValue` stays the - * winning writer's total; clients render a discreet "sources disagree" - * hint next to the number. Minutes are whole (rounded), matching the - * `/api/sleep/night` serialisation. - */ - sleepSourceDiscrepancy: SleepSourceDiscrepancy | null; - trend: "up" | "down" | "flat" | "unknown"; - sparkline: number[]; - updatedAt: string | null; - /** - * v1.4.33 — total readings the user has ever - * logged for this metric, irrespective of the 7-day sparkline - * window. The dashboard tile keeps showing whenever `allTimeCount > 0` - * so a user with valid historical data isn't surprised by a tile - * disappearing during a logging gap. Distinct from `sparkline.length` - * which only carries the trailing-7-day points. - */ - allTimeCount: number; - /** - * v1.4.33 — ISO timestamp of the metric's single - * most recent reading. When `allTimeCount > 0` but the latest - * reading is older than 7 days, the iOS tile renders a muted - * "last reading N days ago" caption so the user understands the - * value isn't stale silently. `null` when the metric has no readings - * at all. - */ - lastSeenAt: string | null; - /** - * v1.43 (ios #116 item 3) — which way a change of this metric counts for - * the person, against their own target or a profile default - * (`src/lib/targets/trend-verdict.ts`). - */ - direction?: TrendVerdict | null; - /** v1.43 — `direction` applied to this card's `trend`. */ - trendSentiment?: TrendSentiment; -} - -/** - * The verdict key behind each card: the measurement type it reads, or the - * derived key for the two cards that are not one. - */ -const VERDICT_KEY: Record = { - weight: "WEIGHT", - bloodPressure: "BLOOD_PRESSURE_SYS", - pulse: "PULSE", - bodyFat: "BODY_FAT", - glucose: "BLOOD_GLUCOSE", - sleep: "SLEEP_DURATION", - steps: "ACTIVITY_STEPS", - totalBodyWater: "TOTAL_BODY_WATER", - boneMass: "BONE_MASS", - oxygenSaturation: "OXYGEN_SATURATION", - mood: "MOOD", - bmi: "BMI", -}; - -/** - * Lay the verdict over each card. The reference placed against the band is - * the mean of the card's own seven-day sparkline, else its latest value, the - * seven-day average the web tiles place. Sleep cards are in hours and the - * verdict reads minutes. - */ -function withVerdicts( - cards: MetricCard[], - ctx: Omit, -): MetricCard[] { - const referenceOf = (card: MetricCard | undefined): number | null => { - if (!card) return null; - const spark = card.sparkline.filter((v) => Number.isFinite(v)); - return spark.length > 0 - ? spark.reduce((sum, v) => sum + v, 0) / spark.length - : card.latestValue; - }; - // BMI is judged by the weight it is derived from (the target is a weight). - const weightReference = referenceOf(cards.find((c) => c.kind === "weight")); - return cards.map((card) => { - const reference = card.kind === "bmi" ? weightReference : referenceOf(card); - const direction = resolveTrendVerdict(VERDICT_KEY[card.kind], { - ...ctx, - referenceValue: - reference !== null && card.kind === "sleep" - ? reference * 60 - : reference, - }); - return { - ...card, - direction, - trendSentiment: trendSentimentOfTrend(card.trend, direction), - }; - }); -} - -/** - * v1.5.x — wire i18n keys, not translated strings. - * - * The legacy maps emitted German literals like "Gewicht" / "Schritte" - * which injected into an English iOS UI once the iOS app flipped its - * source language. Clients now resolve these keys against their own - * locale bundle (the PWA via `useTranslations()`, iOS via - * `Localizable.xcstrings`). The server payload becomes language-neutral - * and the response shrinks slightly. - * - * One key per `MetricKind` — symmetric maps so a new kind has to add - * both a title and a unit key (Type-checked: `Record`). - */ -const METRIC_TITLE_KEYS: Record = { - weight: "dashboard.metric.title.weight", - bloodPressure: "dashboard.metric.title.bloodPressure", - pulse: "dashboard.metric.title.pulse", - bodyFat: "dashboard.metric.title.bodyFat", - glucose: "dashboard.metric.title.glucose", - sleep: "dashboard.metric.title.sleep", - steps: "dashboard.metric.title.steps", - totalBodyWater: "dashboard.metric.title.totalBodyWater", - boneMass: "dashboard.metric.title.boneMass", - oxygenSaturation: "dashboard.metric.title.oxygenSaturation", - mood: "dashboard.metric.title.mood", - bmi: "dashboard.metric.title.bmi", -}; - -const METRIC_UNIT_KEYS: Record = { - weight: "dashboard.metric.unit.weight", - bloodPressure: "dashboard.metric.unit.bloodPressure", - pulse: "dashboard.metric.unit.pulse", - bodyFat: "dashboard.metric.unit.bodyFat", - glucose: "dashboard.metric.unit.glucose", - sleep: "dashboard.metric.unit.sleep", - steps: "dashboard.metric.unit.steps", - totalBodyWater: "dashboard.metric.unit.totalBodyWater", - boneMass: "dashboard.metric.unit.boneMass", - oxygenSaturation: "dashboard.metric.unit.oxygenSaturation", - mood: "dashboard.metric.unit.mood", - bmi: "dashboard.metric.unit.bmi", -}; - -/** - * v1.11.4 — sleep sparkline = the trailing-7 nights' TIME-ASLEEP in - * hours, reconstructed per night from the raw stage rows. The generic - * DAY-bucket rollup sparkline can't be used for sleep because it means - * across the per-stage rows (e.g. a 60-min DEEP row and a 240-min CORE - * row average to 150) rather than summing them into a night total. - */ -function buildSleepSparkline( - rows: SleepStageRow[], - tz: string, - priorityJson: unknown, -): number[] { - return reconstructSleepNights(rows, tz, priorityJson) - .filter((n) => n.asleepMinutes > 0) - .map((n) => Math.round((n.asleepMinutes / 60) * 100) / 100) - .slice(-SPARK_DAYS); -} - -function trendOf(values: number[]): MetricCard["trend"] { - if (values.length < 2) return "unknown"; - const first = values[0]; - const last = values[values.length - 1]; - const delta = last - first; - const epsilon = Math.max(1, Math.abs(first) * 0.01); - if (Math.abs(delta) < epsilon) return "flat"; - return delta > 0 ? "up" : "down"; -} + buildDashboardSummary, + buildSummaryContext, +} from "@/lib/dashboard/summary/build"; export const GET = apiHandler(async () => { // v1.37.0 — MANAGE-level read: computed over the whole record, with no @@ -365,7 +102,7 @@ export const GET = apiHandler(async () => { >, `${user.id}|dashboard-summary`, async () => - buildDashboardSummary(user.id, userTz, await buildContext(user)), + buildDashboardSummary(user.id, userTz, await buildSummaryContext(user)), annotate, ); @@ -399,634 +136,3 @@ export const GET = apiHandler(async () => { return apiSuccess({ ...body, metrics }); }); - -interface SummaryBuilderContext { - greetingName: string; - locale: Locale; -} - -async function buildContext(user: { - displayName: string | null; - username: string; - locale: string | null; -}): Promise { - const greetingName = user.displayName ?? user.username; - // The greeting salutation and the streak label are the two strings this - // route still translates server-side, and the native client is its only - // caller — a Bearer request carries no locale cookie and no meaningful - // Accept-Language, so the stored column is the only per-user signal. - // That is exactly the shape `resolveJobLocale` exists for: stored user - // locale, then the operator's configured default, then English. The - // hand-rolled coercion this replaces stopped at English, so a German - // instance whose user never touched the language picker greeted them in - // English while every other string on the same screen was German. - const locale: Locale = await resolveJobLocale(user.locale); - return { greetingName, locale }; -} - -async function buildDashboardSummary( - userId: string, - userTz: string, - ctx: SummaryBuilderContext, -) { - const now = new Date(); - // REG-11 (v1.4.44): the trailing-7-day pivot used to anchor the - // latest-reading + sparkline filters is gone — both sub-queries now - // take the most recent N rows per type regardless of age. Only the - // streak window still anchors on a calendar slice. - const streakWindowStart = new Date( - now.getTime() - STREAK_WINDOW_DAYS * 86_400_000, - ); - - // Derived from canonical enum so a new measurement type is auto-included - // (V3 audit: enum drift cousins). Per-kind display blocks below decide - // which types render as MetricCards. - const measurementTypes = [ - ...measurementTypeEnum.options, - ] as MeasurementType[]; - - // v1.4.38 — per-sub-query wall-clock timing for prod observability. - // Captured on the cache-miss path; the cache-hit path skips this whole - // builder. Reported under `meta.dashboard.sub_*_ms` so the next - // perf-verify can attribute a regression to a specific sub-query - // without re-instrumenting the route. - const timings: Record = {}; - const time = async ( - label: string, - builder: () => Promise, - ): Promise => { - const t0 = Date.now(); - const result = await builder(); - timings[`dashboard.sub_${label}_ms`] = Date.now() - t0; - return result; - }; - - // v1.4.38 — six bounded sub-queries replace the legacy 4 - // unbounded ones. Row counts are now: - // - latestEver: ≤ N metric types (one indexed lookup per type via - // LATERAL, REG-11: dropped the 7-day window so a - // stale-but-valid historical reading still feeds the tile) - // - sparkBuckets: ≤ SPARK_DAYS × N metric types (typically <80, - // REG-11: taken via ROW_NUMBER window, no calendar - // filter, so a 60-day-old metric still paints a chart) - // - allTimeAggregate: ≤ N metric types, summed from the DAY rollups - // - medsToday: today-window tally via the shared projector - // (≤ daily intake schedule count, unchanged semantics) - // - streakActivity: ≤ daily intake count × 365 (unchanged) - // - measurementStreakDays: ≤ 365 (was: every raw row in 365d) - const [ - latestEver, - sparkBuckets, - allTimeAggregate, - medsToday, - streakActivity, - measurementStreakDays, - sleepStageRows, - ] = await Promise.all([ - // Both reads live in `@/lib/dashboard/summary-reads` — the SQL is a - // contract the integration suite exercises directly against Postgres, - // and each carries the reason it names the type list at its - // definition. Short version: PostgreSQL 16 has no index skip scan, so - // "one row per type" has to be driven FROM the type list or it - // degrades to a full pass over the account. - time("latestEver", () => readLatestEver(prisma, userId, measurementTypes)), - time("sparkline", () => - readSparkBuckets(prisma, userId, measurementTypes, SPARK_DAYS), - ), - // Counted from the DAY rollups where they cover the days, not by one - // index entry per reading (see `readAllTimeCounts`). - time("allTime", () => - readAllTimeCounts(prisma, userId, measurementTypes, now), - ), - // v1.4.39 — the dashboard compliance tile pulled - // `MedicationIntakeEvent` rows for the today-window only. Daily - // schedules (`daysOfWeek: null`) had no row to read until the - // reminder worker entered the RED phase at the end of the dose - // window, so the iOS Dashboard tile fell to "Heute nichts geplant" - // even when the intake route (post-`expandTodayIntakes` fix) was - // already projecting + backfilling correctly. The shared - // `buildMedsTodayBlock` runs the same idempotent projection - // (`skipDuplicates: true` so a concurrent intake-route hit can't - // race a duplicate row in) before its today-window tally, so this - // route and the dashboard snapshot count the identical row set. - time("medsToday", () => buildMedsTodayBlock(prisma, userId, userTz, now)), - time("streakIntakes", () => - prisma.medicationIntakeEvent.findMany({ - where: { - userId, - // v1.7.0 sync — exclude tombstoned rows from streak counting. - deletedAt: null, - scheduledFor: { gte: streakWindowStart }, - OR: [{ takenAt: { not: null } }, { skipped: true }], - }, - select: { takenAt: true, scheduledFor: true }, - }), - ), - time("streakDays", () => - readActivityDayKeys(prisma, userId, userTz, streakWindowStart, now), - ), - // v1.11.4 — raw per-stage SLEEP_DURATION rows for the night-total - // tile. `SLEEP_DURATION` is stored one row per STAGE per night - // (minutes), so the single-most-recent row (the `latestEver` path - // above) is just ONE stage, not the night. The sleep tile instead - // sums the asleep stages of the latest night via - // `summarizeSleepNights`. Bounded to the streak window (≈ a year of - // ~5 rows/night) so the read stays small; the headline only needs - // the most-recent night but the window also feeds the iOS sparkline - // night totals. - time("sleepNights", () => - prisma.measurement.findMany({ - where: { - userId, - type: "SLEEP_DURATION", - deletedAt: null, - measuredAt: { gte: streakWindowStart }, - }, - orderBy: { measuredAt: "asc" }, - // Writer-level collapse: two HealthKit apps behind one source - // (watch stages vs phone in-bed) must not blend into one night. - select: { - value: true, - measuredAt: true, - sleepStage: true, - source: true, - deviceType: true, - }, - }), - ), - ]); - - // v1.11.4 — the user's sleep source-priority ladder, used to collapse a - // dual-source night (e.g. WHOOP + Apple Health) to one canonical source - // before the per-night reconstruction sums it. v1.17.0 — the baseline - // profile rides along (one read) so the sleep-rhythm block resolves the - // age-based sleep need the cumulative-debt deficit uses. - const [sleepPriorityJson, sleepProfile, moodSeries] = await Promise.all([ - loadUserSourcePriority(userId), - loadBaselineProfile(prisma, userId), - // The mood card reads the SAME canonical daily series the web mood - // analytics and the dashboard snapshot read (`buildMoodDailySeries`), - // rollup fast-path included — never a second aggregation. - time("moodSeries", () => buildMoodDailySeries(userId, prisma)), - ]); - - // BMI is derived, not measured: it needs the profile height loaded above, - // so it cannot join the round before it. `computeBmi` is the one canonical - // implementation (the same one the web vitals dashboard renders through - // the derived-metric dispatch) — the route resolves the figure and the - // client consumes it, rather than every client re-deriving weight / m². - const bmiDerived = await time("bmi", () => - computeBmi(userId, sleepProfile, { now }), - ); - - // Per-type metadata lookup — typed Map so a metric with no readings - // at all falls through `metaForType` to the `{ allTimeCount: 0, - // lastSeenAt: null }` default. - const allTimeByType = new Map< - MeasurementType, - { allTimeCount: number; lastSeenAt: Date | null } - >(); - // The newest reading is the one `latestEver` already read per type; its - // instant is the metric's last-seen time. - const lastSeenByType = new Map( - latestEver.map((row) => [row.type, row.measured_at]), - ); - for (const row of allTimeAggregate) { - allTimeByType.set(row.type, { - allTimeCount: row.count, - lastSeenAt: lastSeenByType.get(row.type) ?? null, - }); - } - function metaForType(type: MeasurementType): { - allTimeCount: number; - lastSeenAt: string | null; - } { - const slot = allTimeByType.get(type); - return { - allTimeCount: slot?.allTimeCount ?? 0, - lastSeenAt: slot?.lastSeenAt?.toISOString() ?? null, - }; - } - - // v1.4.38 — activity-day set assembled from the bounded streak - // queries. The measurement side arrives as local YYYY-MM-DD keys, the - // same keys `userDayKey(measuredAt, userTz)` produces for the intakes. - const activityDays = new Set(measurementStreakDays); - for (const e of streakActivity) { - activityDays.add(userDayKey(e.takenAt ?? e.scheduledFor, userTz)); - } - - const streak = computeStreak(activityDays, userTz); - - // v1.4.38 — per-type latest (one row per type) + sparkline - // (bucket means per day) assembled from the two new SQL aggregates. - // REG-11 (v1.4.44): `latestEver` carries the most recent reading - // regardless of age — the iOS tile shows the historical value plus a - // muted "Letzter Wert vor Xd" caption rather than nothing at all. - const latestByType = new Map(); - for (const row of latestEver) { - latestByType.set(row.type, { - value: Number(row.value), - at: new Date(row.measured_at), - }); - } - // v1.4.39 — cumulative tiles paint the daily SUM; spot tiles - // keep the daily MEAN. Reading `sum_value` directly off the rollup - // row eliminates the legacy `mean * count` reconstruction; the - // fallback covers pre-v1.4.39 NULL rows during the boot-backfill - // convergence window. - const sparkByType = new Map(); - for (const row of sparkBuckets) { - const list = sparkByType.get(row.type) ?? []; - // QA Simplifier (v1.4.39): flatten the cumulative-vs-spot ternary - // into a named branch per the CLAUDE.md "no nested ternaries" - // rule. Cumulative tiles paint the daily SUM; spot tiles keep the - // daily MEAN. The legacy NULL fallback covers pre-v1.4.39 rollup - // rows the boot-backfill hasn't refreshed yet. - const cumulativePoint = - row.sum_value !== null - ? Number(row.sum_value) - : Number(row.mean) * Number(row.count); - const point = CUMULATIVE_HK_TYPES.has(row.type) - ? cumulativePoint - : Number(row.mean); - list.push(point); - sparkByType.set(row.type, list); - } - - function latestOf(type: MeasurementType): { value: number; at: Date } | null { - return latestByType.get(type) ?? null; - } - - function sparkOf(type: MeasurementType): number[] { - return sparkByType.get(type) ?? []; - } - - // v1.11.4 — collapse the per-stage SLEEP_DURATION rows into per-night - // asleep totals. The sleep tile's headline is last night's TIME ASLEEP - // (CORE/light + DEEP + REM, excluding IN_BED + AWAKE), emitted in HOURS - // with an explicit `unit: "h"`, replacing the single-stage minutes the - // `latestEver` read used to surface. - const sleepSummary = summarizeSleepNights( - sleepStageRows as SleepStageRow[], - userTz, - sleepPriorityJson, - ); - - // v1.17.0 — server-authoritative sleep-rhythm DTO (sleep-debt + chronotype) - // off the SAME canonical reconstruction the Sleep page + Sleep Score read, - // so iOS renders identical values. Reuses the rows already loaded above — - // `reconstructNights` adapts them into the asleep + wall-clock-midpoint - // shape the foundation modules consume, then `computeSleepRhythmFromNights` - // forwards them to `computeSleepDebt` / `computeChronotype` (the one place - // the math lives). Both signals carry a calm partial / learning state below - // their night thresholds — never an assertion off thin data. - const sleepRhythm: SleepRhythmDto = computeSleepRhythmFromNights( - reconstructNights( - sleepStageRows as SleepStageRow[], - userTz, - sleepPriorityJson, - ), - sleepNeedMinutes(sleepProfile.ageYears), - "COMPUTED", - await loadFreeWeekdaySpans(userId), - ); - - const metrics: MetricCard[] = []; - - // v1.4.33 — every emitted card now carries - // `allTimeCount` + `lastSeenAt` so the iOS tile renderer can keep - // a metric visible during a logging gap (gate on `allTimeCount > 0`) - // and paint a muted "Letzter Wert vor Xd" caption when the most - // recent reading is older than the 7-day sparkline window. - - // Weight - { - const latest = latestOf("WEIGHT"); - const spark = sparkOf("WEIGHT"); - const meta = metaForType("WEIGHT"); - metrics.push({ - id: "weight", - kind: "weight", - titleKey: METRIC_TITLE_KEYS.weight, - latestValue: latest?.value ?? null, - secondaryValue: null, - unitKey: METRIC_UNIT_KEYS.weight, - unit: null, - sleepStages: null, - sleepSourceDiscrepancy: null, - trend: trendOf(spark), - sparkline: spark, - updatedAt: latest?.at?.toISOString() ?? meta.lastSeenAt, - allTimeCount: meta.allTimeCount, - lastSeenAt: meta.lastSeenAt, - }); - } - - // Blood pressure (paired sys/dia) — REG-11 (v1.4.44): gate the emit - // on `latestSys || latestDia || bpAllTimeCount > 0` so an account - // that has never logged BP doesn't get an empty placeholder tile. - // Mirrors the body-fat gate below. - { - const latestSys = latestOf("BLOOD_PRESSURE_SYS"); - const latestDia = latestOf("BLOOD_PRESSURE_DIA"); - const sysSpark = sparkOf("BLOOD_PRESSURE_SYS"); - const sysMeta = metaForType("BLOOD_PRESSURE_SYS"); - const diaMeta = metaForType("BLOOD_PRESSURE_DIA"); - // BP is a paired metric — the tile is "alive" whenever either side - // of the pair has history. Sum the count to reflect total readings - // and pick the most recent `_max` so the staleness hint follows - // whichever side is freshest. - const bpAllTimeCount = sysMeta.allTimeCount + diaMeta.allTimeCount; - const bpLastSeenAt = ((): string | null => { - const sysAt = sysMeta.lastSeenAt; - const diaAt = diaMeta.lastSeenAt; - if (!sysAt) return diaAt; - if (!diaAt) return sysAt; - return sysAt >= diaAt ? sysAt : diaAt; - })(); - if (latestSys || latestDia || bpAllTimeCount > 0) { - metrics.push({ - id: "bp", - kind: "bloodPressure", - titleKey: METRIC_TITLE_KEYS.bloodPressure, - latestValue: latestSys?.value ?? null, - secondaryValue: latestDia?.value ?? null, - unitKey: METRIC_UNIT_KEYS.bloodPressure, - unit: null, - sleepStages: null, - sleepSourceDiscrepancy: null, - trend: trendOf(sysSpark), - sparkline: sysSpark, - updatedAt: - latestSys?.at?.toISOString() ?? - latestDia?.at?.toISOString() ?? - bpLastSeenAt, - allTimeCount: bpAllTimeCount, - lastSeenAt: bpLastSeenAt, - }); - } - } - - // Pulse — REG-11 (v1.4.44): gate the emit on `latest || allTimeCount - // > 0` so an account that has never logged pulse doesn't get an empty - // placeholder tile. Mirrors the body-fat gate below. - { - const latest = latestOf("PULSE"); - const spark = sparkOf("PULSE"); - const meta = metaForType("PULSE"); - if (latest || meta.allTimeCount > 0) { - metrics.push({ - id: "pulse", - kind: "pulse", - titleKey: METRIC_TITLE_KEYS.pulse, - latestValue: latest?.value ?? null, - secondaryValue: null, - unitKey: METRIC_UNIT_KEYS.pulse, - unit: null, - sleepStages: null, - sleepSourceDiscrepancy: null, - trend: trendOf(spark), - sparkline: spark, - updatedAt: latest?.at?.toISOString() ?? meta.lastSeenAt, - allTimeCount: meta.allTimeCount, - lastSeenAt: meta.lastSeenAt, - }); - } - } - - // Body fat — v1.4.33 widens the emit gate from `latestOf` (only - // fires when the 7-day window has a reading) to `allTimeCount > 0` - // so a tile doesn't disappear during a fortnight without a fresh - // reading. - { - const latest = latestOf("BODY_FAT"); - const spark = sparkOf("BODY_FAT"); - const meta = metaForType("BODY_FAT"); - if (latest || meta.allTimeCount > 0) { - metrics.push({ - id: "bodyFat", - kind: "bodyFat", - titleKey: METRIC_TITLE_KEYS.bodyFat, - latestValue: latest?.value ?? null, - secondaryValue: null, - unitKey: METRIC_UNIT_KEYS.bodyFat, - unit: null, - sleepStages: null, - sleepSourceDiscrepancy: null, - trend: trendOf(spark), - sparkline: spark, - updatedAt: latest?.at.toISOString() ?? meta.lastSeenAt, - allTimeCount: meta.allTimeCount, - lastSeenAt: meta.lastSeenAt, - }); - } - } - - // Optional cards — emitted whenever the user has *ever* logged the - // metric. Up to v1.4.32 the gate was `latestOf(type)` (only fires - // when the trailing 7-day window has a reading); v1.4.33 widens to - // the all-time count so a glucose tile that hasn't been touched in - // 10 days still renders with the historical value visible to the - // iOS client (paired with the lastSeenAt caption). - for (const [type, kind] of [ - ["BLOOD_GLUCOSE", "glucose"], - ["SLEEP_DURATION", "sleep"], - ["ACTIVITY_STEPS", "steps"], - ["TOTAL_BODY_WATER", "totalBodyWater"], - ["BONE_MASS", "boneMass"], - ["OXYGEN_SATURATION", "oxygenSaturation"], - ] as const) { - const latest = latestOf(type); - const meta = metaForType(type); - if (!latest && meta.allTimeCount === 0) continue; - - // v1.11.4 — sleep is night-aggregated, not single-stage. Emit the - // latest night's TIME ASLEEP in HOURS (float) with an explicit - // `unit: "h"`, the per-stage breakdown, and a sparkline of the - // trailing nights' asleep hours. Every other kind keeps the - // single-row latest value + canonical-unit i18n key. - if (kind === "sleep") { - const night = sleepSummary.latestNight; - const toHours = (minutes: number): number => - Math.round((minutes / 60) * 100) / 100; - // Sparkline = trailing nights' asleep hours from the night - // reconstruction (the DAY-bucket rollup sparkline blends stages - // and would be misleading for sleep). - const nightSpark = buildSleepSparkline( - sleepStageRows as SleepStageRow[], - userTz, - sleepPriorityJson, - ); - const stageHours: Partial> | null = night - ? Object.fromEntries( - Object.entries(night.stages).map(([s, m]) => [s, toHours(m)]), - ) - : null; - metrics.push({ - id: kind, - kind, - titleKey: METRIC_TITLE_KEYS[kind], - latestValue: night ? toHours(night.asleepMinutes) : null, - secondaryValue: null, - unitKey: METRIC_UNIT_KEYS[kind], - unit: "h", - sleepStages: - stageHours && Object.keys(stageHours).length > 0 ? stageHours : null, - // Additive, observational: the headline night's MAIN session saw - // two writer buckets with clearly different asleep totals. Round - // to whole minutes, matching the `/api/sleep/night` serialisation. - sleepSourceDiscrepancy: night?.sourceDiscrepancy - ? { - deltaMinutes: Math.round(night.sourceDiscrepancy.deltaMinutes), - sources: night.sourceDiscrepancy.sources.map((b) => ({ - source: b.source, - deviceType: b.deviceType, - asleepMinutes: Math.round(b.asleepMinutes), - })), - } - : null, - trend: trendOf(nightSpark), - sparkline: nightSpark, - updatedAt: night?.measuredAt.toISOString() ?? meta.lastSeenAt, - allTimeCount: meta.allTimeCount, - lastSeenAt: night?.measuredAt.toISOString() ?? meta.lastSeenAt, - }); - continue; - } - - const spark = sparkOf(type); - metrics.push({ - id: kind, - kind, - titleKey: METRIC_TITLE_KEYS[kind], - latestValue: latest?.value ?? null, - secondaryValue: null, - unitKey: METRIC_UNIT_KEYS[kind], - unit: null, - sleepStages: null, - sleepSourceDiscrepancy: null, - trend: trendOf(spark), - sparkline: spark, - updatedAt: latest?.at.toISOString() ?? meta.lastSeenAt, - allTimeCount: meta.allTimeCount, - lastSeenAt: meta.lastSeenAt, - }); - } - - // Mood — the one metric on this payload that is not a `Measurement`. The - // daily series is the canonical `buildMoodDailySeries` output (rollup - // fast-path, 1..5 scale, one entry per day with an average score), the - // same series the web mood analytics and the dashboard snapshot render. - // Emitted only when the account has ever logged a mood, matching the - // `allTimeCount === 0` gate every optional card above uses. - if (moodSeries.entryCount > 0) { - const spark = moodSeries.entries.slice(-SPARK_DAYS).map((e) => e.score); - const latestEntry = moodSeries.entries[moodSeries.entries.length - 1]; - // The series carries the user's LOCAL day keys (`MoodEntry.date`), not - // timestamps, so the only honest instant is the first instant of that - // day in the user's zone. Writing the key as `T00:00Z` instead put the - // instant on the previous calendar day for anyone west of UTC, and a - // client formatting it in its own zone showed today's entry as - // yesterday's. - const latestAt = latestEntry - ? startOfLocalDayKey(latestEntry.date, userTz).toISOString() - : null; - metrics.push({ - id: "mood", - kind: "mood", - titleKey: METRIC_TITLE_KEYS.mood, - latestValue: latestEntry?.score ?? null, - secondaryValue: null, - unitKey: METRIC_UNIT_KEYS.mood, - unit: null, - sleepStages: null, - sleepSourceDiscrepancy: null, - trend: trendOf(spark), - sparkline: spark, - updatedAt: latestAt, - allTimeCount: moodSeries.entryCount, - lastSeenAt: latestAt, - }); - } - - // BMI — derived from the latest weight at the profile height by - // `computeBmi`, the single implementation the web vitals dashboard also - // renders. The card is omitted when the derivation is insufficient - // (no height on the profile, or no weight inside its window): the card - // shape has no "unknown" state to express honestly, and a card with a - // null value would read as a logging gap rather than a missing profile - // field. Clients that want to prompt for a height should do so from the - // profile surface, not from an empty dashboard tile. - if (isDerivedOk(bmiDerived)) { - const spark = bmiDerived.value.series.slice(-SPARK_DAYS); - metrics.push({ - id: "bmi", - kind: "bmi", - titleKey: METRIC_TITLE_KEYS.bmi, - latestValue: bmiDerived.value.bmi, - secondaryValue: null, - unitKey: METRIC_UNIT_KEYS.bmi, - unit: null, - sleepStages: null, - sleepSourceDiscrepancy: null, - trend: trendOf(spark), - sparkline: spark, - // BMI moves exactly when weight does, so the weight metadata is the - // honest recency signal — there is no separate BMI reading. - updatedAt: metaForType("WEIGHT").lastSeenAt, - allTimeCount: metaForType("WEIGHT").allTimeCount, - lastSeenAt: metaForType("WEIGHT").lastSeenAt, - }); - } - - // Shared builder carries the exact tally semantics this tile always - // had (scheduled = every today-window row; taken = `takenAt` set and - // not skipped). The wire shape of `compliance` is locked for iOS. - const scheduledToday = medsToday.scheduledToday; - const takenToday = medsToday.takenToday; - - const t = getServerTranslator(ctx.locale).t; - - // v1.43 — the record's profile and own targets, for the card verdicts. - const verdictProfile = await prisma.user.findUnique({ - where: { id: userId }, - select: { - dateOfBirth: true, - gender: true, - heightCm: true, - thresholdsJson: true, - }, - }); - - annotate({ meta: timings }); - - return { - greeting: { - salutation: t("dashboard.greetingSalutation", { name: ctx.greetingName }), - date: now.toISOString(), - }, - streak: { - currentDays: streak.currentDays, - longest: streak.longest, - label: t("dashboard.streakLabel"), - }, - compliance: { - scheduledToday, - takenToday, - }, - highlightInsight: null, - metrics: withVerdicts(metrics, { - profile: { - dateOfBirth: verdictProfile?.dateOfBirth ?? null, - gender: verdictProfile?.gender ?? null, - heightCm: verdictProfile?.heightCm ?? null, - }, - thresholdsJson: verdictProfile?.thresholdsJson ?? null, - }), - // v1.17.0 — sleep-debt + chronotype, server-authoritative. Same values - // the Sleep page's `/api/sleep/rhythm` read returns. - sleepRhythm, - lastUpdated: now.toISOString(), - }; -} diff --git a/src/app/api/documents/inbound/route.ts b/src/app/api/documents/inbound/route.ts index 86f68721c..2a8c35fb9 100644 --- a/src/app/api/documents/inbound/route.ts +++ b/src/app/api/documents/inbound/route.ts @@ -30,11 +30,13 @@ * * The document is UNTRUSTED (prompt-injection): the server never acts on an * instruction inside it. Storing it does nothing with its contents. + * + * The request reading, the stored-row answers and the list read live in + * `src/lib/documents/` (`upload-request.ts`, `upload-answer.ts`, + * `vault-list.ts`); the store itself is the shared `ingest.ts`. */ import { Buffer } from "node:buffer"; -import { NextResponse } from "next/server"; - import { apiHandler, isScopedCredential, @@ -50,48 +52,44 @@ import { sanitiseZodIssues, } from "@/lib/api-response"; import { getAiCapability } from "@/lib/ai/capabilities/gate"; -import { listPage } from "@/lib/http/list-query"; -import { prisma } from "@/lib/db"; -import { hashQueryTokens } from "@/lib/documents/content-index"; import { ingestDocument, - isoDateToUtc, personalUploadBucket, UPLOAD_LIMIT_PER_HOUR, UPLOAD_WINDOW_MS, } from "@/lib/documents/ingest"; -import { - loadConditionLinks, - loadDocumentEncounterLinks, -} from "@/lib/documents/links"; -import { - serialiseDocument, - type SerialisableDocument, -} from "@/lib/documents/store"; import { DOCUMENTS_WRITE_SCOPE } from "@/lib/documents/scopes"; import { checkSourceLookupRateLimit, findSourceKey, - type SourceKeyMatch, } from "@/lib/documents/source-key"; import { acquireDocumentUploadSlot, resolveDocumentLimits, resolveDocumentUploadLimitPerHour, } from "@/lib/documents/upload-policy"; +import { storedDocumentBody } from "@/lib/documents/upload-answer"; +import { + aliasLimitResponse, + answerSourceKey, + duplicateResponse, + fileTooLarge, + receiptResponse, + type UploadContext, +} from "@/lib/documents/upload-responses"; +import { + parseQuerySourceKey, + parseUploadMetadata, + readUploadForm, + resolveUploadSourceKey, + type QuerySourceKey, +} from "@/lib/documents/upload-request"; +import { listVaultDocuments } from "@/lib/documents/vault-list"; import { withIdempotency } from "@/lib/idempotency"; -import { BodyTooLargeError, readBoundedBody } from "@/lib/labs/ocr-upload"; -import { annotate } from "@/lib/logging/context"; import { requireModuleEnabled } from "@/lib/modules/gate"; import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit"; -import { - documentCreateSchema, - documentListQuerySchema, - documentSourceKeySchema, - toContentIndexSource, - type DocumentSourceSystemValue, -} from "@/lib/validations/inbound-documents"; -import type { Prisma } from "@/generated/prisma/client"; +import type { IngestResult } from "@/lib/documents/ingest"; +import { documentListQuerySchema } from "@/lib/validations/inbound-documents"; export const dynamic = "force-dynamic"; @@ -101,201 +99,31 @@ export const dynamic = "force-dynamic"; * exact per-file cap is re-enforced on the extracted file bytes below. */ const MULTIPART_OVERHEAD_BYTES = 64 * 1024; -const UPLOAD_BODY_TIMEOUT_MS = 30_000; - -/** §3.2 — 413 fileTooLarge with the configured limit in `meta`. */ -function fileTooLarge(maxFileBytes: number): NextResponse { - return apiError("File is too large.", 413, { - errorCode: "documents.inbound.fileTooLarge", - reason: "fileTooLarge", - maxFileBytes, - }); -} - -/** What a single upload asked for, carried into the annotations. */ -interface UploadContext { - userId: string; - /** A narrow `documents:write` token, not a session or a `["*"]` token. */ - scoped: boolean; - sourceSystem: DocumentSourceSystemValue | null; - aiDeferred: boolean; -} /** - * The receipt a scoped caller gets in place of the stored row: the id and - * whether the upload was a duplicate. A write-only credential holder who - * knows a file's bytes learns that it is stored, not its title, filename, - * links or fact counts. + * A source key may ride the query string. It is answered here, before the + * bucket is charged and before a byte of the body is read: a nightly re-sync + * of an archive HealthLog already holds costs neither. Answers the response + * when the key settles the upload, else the key (or null) to carry on with. */ -function receiptResponse( - id: string | null, - status: 200 | 201, - flags: { duplicate: boolean; deleted?: boolean }, -): NextResponse { - const data = { - id, - duplicate: flags.duplicate, - ...(flags.deleted ? { deleted: true } : {}), - }; - return NextResponse.json( - { - data, - error: null, - ...(flags.duplicate - ? { - meta: { - duplicate: true, - ...(flags.deleted ? { deleted: true } : {}), - }, - } - : {}), - }, - { status }, - ); -} - -/** - * v1.39.2 (#1038) — a re-send of an imported document the person deleted. - * 200 and no new row, for every caller: the deletion was a decision, and an - * importer re-running over the same source must not undo it. `id` is the - * tombstoned row while it exists and null once the purge took it; there is no - * row left to serialise either way, so the answer is the receipt. - */ -function deletedResponse(ctx: UploadContext, id: string | null): NextResponse { - annotate({ - action: { name: "documents.vault.upload" }, - meta: { - documentId: id, - duplicate: true, - deleted: true, - scoped: ctx.scoped, - sourceSystem: ctx.sourceSystem, - aiDeferred: ctx.aiDeferred, - }, - }); - return receiptResponse(id, 200, { duplicate: true, deleted: true }); -} - -/** - * The same bytes have already been sent under too many source keys - * (`MAX_SOURCE_ALIASES_PER_DOCUMENT`). A conflict with what is stored, not a - * rate: nothing is remembered and nothing stored. - */ -function aliasLimitResponse(): NextResponse { - return apiError( - "This file is already stored under too many source ids.", - 409, - { errorCode: "documents.inbound.sourceAliasLimit" }, - ); -} - -/** Answer a source key that is already held: duplicate, or deleted. */ -function answerSourceKey( - ctx: UploadContext, - match: SourceKeyMatch, -): Promise | NextResponse { - return match.state === "live" - ? duplicateResponse(ctx, match.document) - : deletedResponse(ctx, match.id); -} - -/** - * §3.2 — a duplicate upload is NOT an error: return the existing live row - * with `meta.duplicate: true` at the envelope level (the UI toasts "already - * stored" and highlights the row). A scoped caller gets the receipt instead. - */ -async function duplicateResponse( - ctx: UploadContext, - existing: SerialisableDocument, -): Promise { - annotate({ - action: { name: "documents.vault.upload" }, - meta: { - documentId: existing.id, - duplicate: true, - scoped: ctx.scoped, - sourceSystem: ctx.sourceSystem, - aiDeferred: ctx.aiDeferred, - }, - }); - if (ctx.scoped) { - return receiptResponse(existing.id, 200, { duplicate: true }); - } - const userId = ctx.userId; - const [links, visitLinks, groups] = await Promise.all([ - loadConditionLinks(userId, [existing.id]), - loadDocumentEncounterLinks(userId, [existing.id]), - prisma.extractedFact.groupBy({ - by: ["status"], - where: { userId, documentId: existing.id }, - _count: { _all: true }, - }), - ]); - let factCount = 0; - let pendingCount = 0; - for (const g of groups) { - if (g.status !== "REJECTED") factCount += g._count._all; - if (g.status === "PENDING") pendingCount += g._count._all; - } - return NextResponse.json( - { - data: serialiseDocument( - existing, - { factCount, pendingCount }, - links.get(existing.id) ?? [], - false, - null, - false, - visitLinks.get(existing.id) ?? [], - ), - error: null, - meta: { duplicate: true }, - }, - { status: 200 }, - ); -} - -/** Process one admitted upload. The caller owns and releases its memory slot. */ -async function processUpload( +async function answerQuerySourceKey( request: Request, auth: AuthContext, -): Promise { + scoped: boolean, +): Promise { const { user } = auth; - const scoped = isScopedCredential(auth); - - // Opt-in module gate — even a valid Bearer token is refused when the surface - // is off (it ships dark; the user turns it on deliberately). - const gate = await requireModuleEnabled(user.id, "inboundDocuments"); - if (!gate.enabled) return gate.response; - - // A source key may ride the query string. It is answered here, before the - // bucket is charged and before a byte of the body is read: a nightly - // re-sync of an archive HealthLog already holds costs neither. Both halves - // or neither; half a key is a mistake worth saying so about. - const url = new URL(request.url); - const querySystem = url.searchParams.get("sourceSystem"); - const queryId = url.searchParams.get("sourceId"); - const queryInstance = url.searchParams.get("sourceInstance"); - let queryKey: { - sourceSystem: DocumentSourceSystemValue; - sourceId: string; - sourceInstance?: string; - } | null = null; - if (querySystem !== null || queryId !== null) { - const parsedKey = documentSourceKeySchema.safeParse({ - sourceSystem: querySystem ?? undefined, - sourceId: queryId ?? undefined, - sourceInstance: queryInstance ?? undefined, - }); - if (!parsedKey.success) { - return apiValidationError( - "Invalid source key", - sanitiseZodIssues(parsedKey.error.issues), - 422, - { errorCode: "documents.inbound.invalidMetadata" }, - ); - } - queryKey = parsedKey.data; + const query = parseQuerySourceKey(new URL(request.url)); + if (query.kind === "invalid") { + return apiValidationError( + "Invalid source key", + sanitiseZodIssues(query.issues), + 422, + { errorCode: "documents.inbound.invalidMetadata" }, + ); + } + const queryKey: QuerySourceKey | null = + query.kind === "key" ? query.key : null; + if (queryKey) { // Metered like the lookup route, on the same bucket: the check stores // nothing and costs no upload slot, but it is not a free oracle either. const lookupRl = await checkSourceLookupRateLimit( @@ -331,6 +159,71 @@ async function processUpload( } } + return queryKey; +} + +/** The answer to an upload the ingest did not store. */ +function answerNotStored( + ctx: UploadContext, + result: Exclude, +): Response | Promise { + const scoped = ctx.scoped; + switch (result.kind) { + case "sourceKey": + return answerSourceKey(ctx, result.match); + case "tooLarge": + return fileTooLarge(result.maxFileBytes); + case "empty": + return apiError("Uploaded file is empty", 422, { + errorCode: "documents.inbound.invalidMetadata", + }); + case "unsupportedType": + return apiError("This file type is not supported.", 415, { + errorCode: "documents.inbound.fileType", + reason: "unsupportedType", + }); + case "episodeNotFound": + return apiError("Episode not found", 404, { + errorCode: "documents.inbound.episodeNotFound", + }); + case "encounterNotFound": + return apiError("Visit not found", 404, { + errorCode: "documents.inbound.encounterNotFound", + }); + case "aliasLimit": + return aliasLimitResponse(); + case "duplicate": + return duplicateResponse(ctx, result.document); + case "quotaExceeded": + // The figures are the owner's; a write-only token learns only that the + // vault is full. + return apiError("Storage quota exceeded.", 413, { + errorCode: "documents.inbound.quotaExceeded", + reason: "quotaExceeded", + ...(scoped + ? {} + : { quotaBytes: result.quotaBytes, usedBytes: result.usedBytes }), + }); + } +} + +/** Process one admitted upload. The caller owns and releases its memory slot. */ +async function processUpload( + request: Request, + auth: AuthContext, +): Promise { + const { user } = auth; + const scoped = isScopedCredential(auth); + + // Opt-in module gate — even a valid Bearer token is refused when the surface + // is off (it ships dark; the user turns it on deliberately). + const gate = await requireModuleEnabled(user.id, "inboundDocuments"); + if (!gate.enabled) return gate.response; + + const query = await answerQuerySourceKey(request, auth, scoped); + if (query instanceof Response) return query; + const queryKey = query; + // A scoped token draws on its own bucket, keyed on the token (its id rides // `session.id` on the Bearer path), so an import running overnight cannot // use up the allowance the person's own uploads from the web or the phone @@ -367,55 +260,23 @@ async function processUpload( return fileTooLarge(limits.maxFileBytes); } - let formData: FormData; - try { - const bytes = await readBoundedBody(request.body, bodyCap, { - signal: request.signal, - deadline: Date.now() + UPLOAD_BODY_TIMEOUT_MS, + const read = await readUploadForm(request, bodyCap); + if (read.kind === "tooLarge") return fileTooLarge(limits.maxFileBytes); + if (read.kind === "timeout") { + return apiError("Upload body read timed out.", 408, { + errorCode: "documents.inbound.uploadTimeout", + reason: "uploadTimeout", }); - formData = await new Request(request.url, { - method: "POST", - headers: { "content-type": request.headers.get("content-type") ?? "" }, - body: bytes, - }).formData(); - } catch (err) { - if (err instanceof BodyTooLargeError) { - return fileTooLarge(limits.maxFileBytes); - } - if (err instanceof DOMException && err.name === "AbortError") { - return apiError("Upload body read timed out.", 408, { - errorCode: "documents.inbound.uploadTimeout", - reason: "uploadTimeout", - }); - } - return apiError("Invalid multipart body", 400); } + if (read.kind === "invalid") return apiError("Invalid multipart body", 400); + const formData = read.formData; const file = formData.get("file"); if (!(file instanceof File)) { return apiError("Field 'file' must be a file", 422); } - // Optional metadata (title / kind / documentDate / episodeIds / - // encounterIds). The file is read separately; these are the form fields - // beside it. Both id lists may be repeated. - const rawEpisodeIds = formData - .getAll("episodeIds") - .filter((v): v is string => typeof v === "string"); - const rawEncounterIds = formData - .getAll("encounterIds") - .filter((v): v is string => typeof v === "string"); - const parsed = documentCreateSchema.safeParse({ - title: formData.get("title") ?? undefined, - kind: formData.get("kind") ?? undefined, - documentDate: formData.get("documentDate") ?? undefined, - episodeIds: rawEpisodeIds.length > 0 ? rawEpisodeIds : undefined, - encounterIds: rawEncounterIds.length > 0 ? rawEncounterIds : undefined, - sourceSystem: formData.get("sourceSystem") ?? undefined, - sourceId: formData.get("sourceId") ?? undefined, - sourceInstance: formData.get("sourceInstance") ?? undefined, - aiRead: formData.get("aiRead") ?? undefined, - }); + const parsed = parseUploadMetadata(formData); if (!parsed.success) { return apiValidationError( "Invalid document metadata", @@ -427,30 +288,15 @@ async function processUpload( ); } - // The key from the query string, or from the form fields; if a caller sends - // both they have to agree. - const formSystem = parsed.data.sourceSystem ?? null; - const formId = formSystem ? (parsed.data.sourceId ?? null) : null; - const formInstance = formId ? (parsed.data.sourceInstance ?? null) : null; - if ( - queryKey && - formSystem !== null && - (formSystem !== queryKey.sourceSystem || - formId !== queryKey.sourceId || - (formInstance !== null && - formInstance !== (queryKey.sourceInstance ?? null))) - ) { + const key = resolveUploadSourceKey(queryKey, parsed.data); + if (key.kind === "mismatch") { return apiError( "The source key in the address and in the form differ.", 422, { errorCode: "documents.inbound.invalidMetadata" }, ); } - const sourceSystem = queryKey?.sourceSystem ?? formSystem; - const sourceId = queryKey?.sourceId ?? formId; - const sourceInstance = sourceId - ? (queryKey?.sourceInstance ?? formInstance ?? null) - : null; + const { sourceSystem, sourceId, sourceInstance } = key; const ctx: UploadContext = { userId: user.id, scoped, @@ -505,65 +351,11 @@ async function processUpload( documentAi: () => getAiCapability("documentAi"), }); - switch (result.kind) { - case "sourceKey": - return answerSourceKey(ctx, result.match); - case "tooLarge": - return fileTooLarge(result.maxFileBytes); - case "empty": - return apiError("Uploaded file is empty", 422, { - errorCode: "documents.inbound.invalidMetadata", - }); - case "unsupportedType": - return apiError("This file type is not supported.", 415, { - errorCode: "documents.inbound.fileType", - reason: "unsupportedType", - }); - case "episodeNotFound": - return apiError("Episode not found", 404, { - errorCode: "documents.inbound.episodeNotFound", - }); - case "encounterNotFound": - return apiError("Visit not found", 404, { - errorCode: "documents.inbound.encounterNotFound", - }); - case "aliasLimit": - return aliasLimitResponse(); - case "duplicate": - return duplicateResponse(ctx, result.document); - case "quotaExceeded": - // The figures are the owner's; a write-only token learns only that the - // vault is full. - return apiError("Storage quota exceeded.", 413, { - errorCode: "documents.inbound.quotaExceeded", - reason: "quotaExceeded", - ...(scoped - ? {} - : { quotaBytes: result.quotaBytes, usedBytes: result.usedBytes }), - }); - case "stored": - break; - } + if (result.kind !== "stored") return answerNotStored(ctx, result); const document = result.document; if (scoped) return receiptResponse(document.id, 201, { duplicate: false }); - - const [links, visitLinks] = await Promise.all([ - loadConditionLinks(user.id, [document.id]), - loadDocumentEncounterLinks(user.id, [document.id]), - ]); - return apiSuccess( - serialiseDocument( - document, - { factCount: 0, pendingCount: 0 }, - links.get(document.id) ?? [], - false, - null, - false, - visitLinks.get(document.id) ?? [], - ), - 201, - ); + return apiSuccess(await storedDocumentBody(user.id, document), 201); } /** Authenticate and reserve memory capacity before any request-body read. */ @@ -640,172 +432,6 @@ export const GET = apiHandler(async (request: Request) => { }, ); } - const { - q, - kind, - episodeId, - encounterId, - year, - from, - to, - sort, - order, - cursor, - limit, - } = parsed.data; - const where: Prisma.InboundDocumentWhereInput = { - userId: user.id, - deletedAt: null, - }; - if (kind && kind.length > 0) where.kind = { in: kind }; - if (episodeId) where.conditionLinks = { some: { episodeId } }; - // The visit filter reads the SAME table the visit's own sheet writes, from - // the document side. Filtering rather than post-filtering: a page of fifty - // documents narrowed in Node would page wrongly. - if (encounterId) where.encounterLinks = { some: { encounterId } }; - if (year !== undefined) { - where.documentDate = { - gte: new Date(Date.UTC(year, 0, 1)), - lt: new Date(Date.UTC(year + 1, 0, 1)), - }; - } else if (from || to) { - where.documentDate = { - ...(from ? { gte: isoDateToUtc(from) } : {}), - // inclusive end-of-day for the `to` bound - ...(to ? { lt: new Date(isoDateToUtc(to).getTime() + 86_400_000) } : {}), - }; - } - if (q) { - // Substring match on the short plaintext fields … - const or: Prisma.InboundDocumentWhereInput[] = [ - { title: { contains: q, mode: "insensitive" } }, - { filename: { contains: q, mode: "insensitive" } }, - ]; - // … unioned with a WHOLE-WORD content match over the blind token index. - // The query is tokenised + HMAC'd the same way the index was built, then - // matched with a GIN-accelerated array-overlap (`hasSome` → `&&`). The - // list still never selects the encrypted text — only the opaque hashes are - // touched, in the related table. Degrades silently to title/filename when - // the caller has no indexed documents (no rows overlap). - const hashes = hashQueryTokens(q); - if (hashes.length > 0) { - or.push({ contentIndex: { is: { searchTokens: { hasSome: hashes } } } }); - } - where.OR = or; - } - - // Keyset pagination on the sort column + a stable `id` tiebreak; nullable - // sort columns sort nulls last so undated documents trail. - const primaryOrder: - Prisma.InboundDocumentOrderByWithRelationInput | undefined = - sort === "documentDate" - ? { documentDate: { sort: order, nulls: "last" } } - : sort === "title" - ? { title: { sort: order, nulls: "last" } } - : { createdAt: order }; - const orderBy: Prisma.InboundDocumentOrderByWithRelationInput[] = [ - primaryOrder, - { id: order }, - ]; - - // Hardening: the list NEVER fetches the encrypted blob column — a page of - // 50 rows would otherwise drag up to 50 × cap ciphertext bytes per request. - const { rows: page, cap } = await listPage(limit, (take) => - prisma.inboundDocument.findMany({ - where, - omit: { contentEncrypted: true }, - orderBy, - take, - ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}), - }), - ); - - const hasMore = cap.truncated; - const nextCursor = hasMore ? (page[page.length - 1]?.id ?? null) : null; - - // Fact tallies for the whole page in ONE grouped count — no per-document - // fan-out and no materialising fact-id rows. `factCount` counts every - // non-REJECTED fact (a rejected fact is discarded, not part of the - // document's tally), `pendingCount` the PENDING subset awaiting review. - const factCounts = new Map< - string, - { factCount: number; pendingCount: number } - >(); - if (page.length > 0) { - const groups = await prisma.extractedFact.groupBy({ - by: ["documentId", "status"], - where: { userId: user.id, documentId: { in: page.map((d) => d.id) } }, - _count: { _all: true }, - }); - for (const g of groups) { - const entry = factCounts.get(g.documentId) ?? { - factCount: 0, - pendingCount: 0, - }; - const n = g._count._all; - if (g.status !== "REJECTED") entry.factCount += n; - if (g.status === "PENDING") entry.pendingCount += n; - factCounts.set(g.documentId, entry); - } - } - - // Condition/visit links + the content-index and thumbnail probes for the - // page, all in ONE Promise.all — four independent grouped queries (no - // N+1, never a ciphertext/blob column), and the vault polls this list - // every few seconds so two serialized round-trips were pure added - // latency (A5-4). - const pageIds = page.map((d) => d.id); - const [linkMap, visitLinkMap, indexedRows, thumbRows] = await Promise.all([ - loadConditionLinks(user.id, pageIds), - loadDocumentEncounterLinks(user.id, pageIds), - // Which of the page's documents have a content index (drives the - // searchable status + the provenance the UI reads to tell an AI-read - // document from a locally-indexed one). - page.length > 0 - ? prisma.documentContentIndex.findMany({ - where: { userId: user.id, documentId: { in: pageIds } }, - select: { documentId: true, source: true }, - }) - : Promise.resolve([]), - // Which of the page's documents have a preview thumbnail (gates the - // card's ). Grouped on the 1:1 side table. - page.length > 0 - ? prisma.documentThumbnail.findMany({ - where: { userId: user.id, documentId: { in: pageIds } }, - select: { documentId: true }, - }) - : Promise.resolve([]), - ]); - const indexSources = new Map(); - for (const row of indexedRows) indexSources.set(row.documentId, row.source); - const thumbnailIds = new Set(); - for (const row of thumbRows) thumbnailIds.add(row.documentId); - - annotate({ - action: { name: "documents.inbound.list" }, - meta: { - count: page.length, - sort, - order, - filtered: Boolean( - q || (kind && kind.length > 0) || episodeId || encounterId || year, - ), - }, - }); - - return apiSuccess({ - documents: page.map((doc) => - serialiseDocument( - doc, - factCounts.get(doc.id) ?? { factCount: 0, pendingCount: 0 }, - linkMap.get(doc.id) ?? [], - indexSources.has(doc.id), - toContentIndexSource(indexSources.get(doc.id)), - thumbnailIds.has(doc.id), - visitLinkMap.get(doc.id) ?? [], - ), - ), - nextCursor, - }); + return apiSuccess(await listVaultDocuments(user.id, parsed.data)); }); diff --git a/src/app/api/import/csv/route.ts b/src/app/api/import/csv/route.ts index 97d1e4239..fd5be0457 100644 --- a/src/app/api/import/csv/route.ts +++ b/src/app/api/import/csv/route.ts @@ -1,7 +1,5 @@ -import { prisma } from "@/lib/db"; import { apiHandler, requireAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; -import { auditLog } from "@/lib/auth/audit"; import { apiSuccess, apiError, @@ -10,83 +8,14 @@ import { returnAllZodIssues, } from "@/lib/api-response"; import { csvImportQuerySchema } from "@/lib/validations/record-import"; -import { encryptNote, readNote } from "@/lib/crypto/note-cipher"; import { checkRateLimit } from "@/lib/rate-limit"; import { NextRequest } from "next/server"; -import { - parseCsvMeasurements, - type CsvRowResult, -} from "@/lib/import/csv-measurements"; -import { afterMeasurementMutation } from "@/lib/rollups/after-measurement-mutation"; -import type { - MeasurementType, - GlucoseContext, -} from "@/generated/prisma/client"; -import { Prisma } from "@/generated/prisma/client"; -import type { NormalisedMeasurementRow } from "@/lib/import/csv-measurements"; -import { emitInsertedMeasurementArrivals } from "@/lib/arrivals/measurement-emit"; -import { maybeEnqueueMorningRefresh } from "@/lib/daily/morning-refresh-trigger"; - -/** Keys per dedup probe: well under Postgres's bind-parameter cap. */ -const LOOKUP_CHUNK = 1000; -/** Rows per create call / per update batch transaction. */ -const WRITE_CHUNK = 200; - -function chunked(items: readonly T[], size: number): T[][] { - const out: T[][] = []; - for (let i = 0; i < items.length; i += size) { - out.push(items.slice(i, i + size)); - } - return out; -} - -/** Group rows' probe keys by measurement type, de-duplicated. */ -function groupByType( - rows: ReadonlyArray<{ row: NormalisedMeasurementRow }>, - keyOf: (r: { row: NormalisedMeasurementRow }) => K, -): Map { - const byType = new Map>(); - for (const r of rows) { - const type = r.row.type as MeasurementType; - const key = keyOf(r); - const bucket = byType.get(type) ?? new Map(); - bucket.set(key instanceof Date ? String(key.getTime()) : String(key), key); - byType.set(type, bucket); - } - return new Map([...byType].map(([type, keys]) => [type, [...keys.values()]])); -} +import { parseCsvMeasurements } from "@/lib/import/csv-measurements"; +import { runCsvImport, type CsvOkRow } from "@/lib/import/csv-write"; /** - * True when a stored IMPORT row already holds exactly what this line would - * write: same value, unit, instant, glucose context and note, and live. Such - * a line is a duplicate, not an update. - */ -function sameStoredRow( - stored: { - value: number; - unit: string; - measuredAt: Date; - glucoseContext: GlucoseContext | null; - notes: string | null; - notesEncrypted: Uint8Array | null; - deletedAt: Date | null; - }, - m: NormalisedMeasurementRow, -): boolean { - return ( - stored.deletedAt === null && - stored.value === m.value && - stored.unit === m.unit && - stored.measuredAt.getTime() === m.measuredAt.getTime() && - stored.glucoseContext === - ((m.glucoseContext as GlucoseContext | undefined) ?? null) && - readNote(stored.notesEncrypted, stored.notes) === (m.notes || null) - ); -} - -/** - * CSV measurement import (v1.17.1). + * CSV measurement import. * * The cold-start escape hatch for self-hosters migrating from a spreadsheet * or another tracker. Reuses the `/api/import` write loop + rollup re-fold; @@ -101,7 +30,8 @@ function sameStoredRow( * WITHOUT writing — a trust win for a bulk cold-start import. * * Response: `{ inserted, updated, skipped, total, dryRun, rows: [{line, - * status, reason}] }`, mirroring the batch route's per-entry envelope. + * status, reason}] }`, mirroring the batch route's per-entry envelope. The + * batched writer lives in `src/lib/import/csv-write.ts`. */ const MAX_CSV_BYTES = 16 * 1024 * 1024; @@ -150,8 +80,7 @@ export const POST = apiHandler(async (request: NextRequest) => { } const okRows = parsed.rows.filter( - (r): r is CsvRowResult & { row: NonNullable } => - r.status === "ok" && r.row !== undefined, + (r): r is CsvOkRow => r.status === "ok" && r.row !== undefined, ); if (okRows.length > MAX_ROWS) { return apiError( @@ -160,357 +89,13 @@ export const POST = apiHandler(async (request: NextRequest) => { ); } - const userId = user.id; - let inserted = 0; - let updated = 0; - let skipped = parsed.rows.filter((r) => r.status === "skipped").length; - - const touchedMeasurements: Array<{ - type: MeasurementType; - measuredAt: Date; - }> = []; - - // Per-line write outcome for rows that reached the DB. Keyed by source line - // so the response envelope can surface `inserted` / `updated` / `duplicate` - // distinctly without polluting the parse-time `CsvRowReason`. - const writeOutcome = new Map(); - - if (!dryRun) { - // Batched write. The previous shape ran one serial `upsert` (or `create`) - // per accepted row — N round-trips for an N-row file, which on a - // few-thousand-row import approaches the request timeout. This adopts the - // fast shape `/measurements/batch` uses: pre-fetch the existing rows under - // the IMPORT source in one query per dedup-key shape, partition into bulk - // insert / update sets, then `createMany` + grouped `updateMany`. The - // per-line inserted/updated/duplicate envelope is reconstructed from the - // partition, so the response contract is unchanged. - - const buildCreateData = ( - m: NormalisedMeasurementRow, - ): Prisma.MeasurementCreateManyInput => ({ - userId, - type: m.type as MeasurementType, - value: m.value, - unit: m.unit, - source: "IMPORT", - ...(m.externalId ? { externalId: m.externalId } : {}), - measuredAt: m.measuredAt, - notes: null, - notesEncrypted: encryptNote(m.notes ?? null), - glucoseContext: (m.glucoseContext as GlucoseContext | undefined) ?? null, - }); - - const extRows = okRows.filter((r) => r.row.externalId); - const plainRows = okRows.filter((r) => !r.row.externalId); - - // --- externalId rows: idempotent upsert on - // (userId, type, source=IMPORT, externalId). One probe per type and chunk - // fetches every pre-existing key with the columns an update would write, - // so a row whose stored values already match is left alone. - const extExisting = new Map< - string, - { - value: number; - unit: string; - measuredAt: Date; - glucoseContext: GlucoseContext | null; - notes: string | null; - notesEncrypted: Uint8Array | null; - deletedAt: Date | null; - } - >(); - for (const [type, ids] of groupByType( - extRows, - (r) => r.row.externalId as string, - )) { - for (const chunk of chunked(ids, LOOKUP_CHUNK)) { - const found = await prisma.measurement.findMany({ - where: { - userId, - source: "IMPORT", - type, - externalId: { in: chunk }, - }, - select: { - externalId: true, - value: true, - unit: true, - measuredAt: true, - glucoseContext: true, - notes: true, - notesEncrypted: true, - deletedAt: true, - }, - }); - for (const row of found) { - extExisting.set(`${type}::${row.externalId}`, row); - } - } - } - - // Partition in file order, honouring in-file repeats: the first sighting - // of a brand-new key inserts, every later sighting (or any sighting of a - // key already in the DB) updates in place. The last value for a key wins, - // matching the old sequential-upsert overwrite. Insert/update maps are - // keyed by (type, externalId) so a repeated key collapses to one write. - // A key already in the DB whose stored row equals this line is a - // duplicate: no write, reported like the natural-key duplicates below. - // That is what makes re-importing the same file cheap — a second upload - // of an unchanged 9 000-row export writes nothing. - const seenExt = new Set(); - const extInserts = new Map(); - const extUpdates = new Map(); - for (const result of extRows) { - const m = result.row; - const key = `${m.type}::${m.externalId}`; - const stored = extExisting.get(key); - if (stored && !extUpdates.has(key) && sameStoredRow(stored, m)) { - skipped++; - writeOutcome.set(result.line, "duplicate"); - continue; - } - if (stored) { - extUpdates.set(key, m); - updated++; - writeOutcome.set(result.line, "updated"); - } else if (seenExt.has(key)) { - // New-to-DB key seen again in this file: still a single create with - // the latest value, but this line reads as an update just as the - // sequential upsert did. - extInserts.set(key, m); - updated++; - writeOutcome.set(result.line, "updated"); - } else { - seenExt.add(key); - extInserts.set(key, m); - inserted++; - writeOutcome.set(result.line, "inserted"); - } - touchedMeasurements.push({ - type: m.type as MeasurementType, - measuredAt: m.measuredAt, - }); - // An update that moves the reading to another instant leaves the day - // it came from: that day is re-folded too, or the old day keeps a - // reading that no longer exists there. - if (stored && stored.measuredAt.getTime() !== m.measuredAt.getTime()) { - touchedMeasurements.push({ - type: m.type as MeasurementType, - measuredAt: stored.measuredAt, - }); - } - } - - // --- externalId-less rows: create-only, deduped on the natural unique key - // (userId, type, measuredAt, source=IMPORT, sleepStage=null). A collision - // — against the DB or an earlier row in this same file — is a duplicate, - // never an overwrite (each sample is a canonical reading). Probed per type - // and chunk, like the externalId rows. - const plainExistingSet = new Set(); - for (const [type, instants] of groupByType( - plainRows, - (r) => r.row.measuredAt, - )) { - for (const chunk of chunked(instants, LOOKUP_CHUNK)) { - const found = await prisma.measurement.findMany({ - where: { - userId, - source: "IMPORT", - type, - measuredAt: { in: chunk }, - }, - select: { measuredAt: true }, - }); - for (const row of found) { - plainExistingSet.add(`${type}::${row.measuredAt.getTime()}`); - } - } - } - const seenPlain = new Set(); - const plainInserts: NormalisedMeasurementRow[] = []; - for (const result of plainRows) { - const m = result.row; - const key = `${m.type}::${m.measuredAt.getTime()}`; - if (plainExistingSet.has(key) || seenPlain.has(key)) { - skipped++; - writeOutcome.set(result.line, "duplicate"); - } else { - seenPlain.add(key); - plainInserts.push(m); - inserted++; - writeOutcome.set(result.line, "inserted"); - touchedMeasurements.push({ - type: m.type as MeasurementType, - measuredAt: m.measuredAt, - }); - } - } - - // Bulk write in bounded chunks, each its own short transaction. - // - // This used to be ONE interactive transaction around every create chunk - // AND one `updateMany` per re-imported row. Re-importing a 9 000-row file - // with an externalId column meant 9 000 serial updates inside it, which - // outlived Prisma's 5 s interactive-transaction timeout and ended in a - // 500. A chunk now commits on its own: creates go through - // `createManyAndReturn` (skipDuplicates absorbs a concurrent - // double-submit), updates through one batch transaction per chunk. A - // failure part-way leaves the earlier chunks written, and that is safe to - // retry: a re-upload of the same file skips what landed (unchanged - // externalId rows and natural-key duplicates are not written again). - const toCreate: Prisma.MeasurementCreateManyInput[] = [ - ...[...extInserts.values()].map(buildCreateData), - ...plainInserts.map(buildCreateData), - ]; - let createdCount = 0; - const createdRows: Array<{ - id: string; - type: MeasurementType; - measuredAt: Date; - }> = []; - // The re-fold runs whatever happens to the writes. A chunk that fails - // part-way leaves the earlier chunks written, and a retry reads those - // rows as duplicates and touches nothing: without the re-fold here their - // days would keep the rollups from before the import for good. Re-folding - // a (type, day) nothing landed on is harmless. - try { - for (const chunk of chunked(toCreate, WRITE_CHUNK)) { - const rows = await prisma.measurement.createManyAndReturn({ - data: chunk, - skipDuplicates: true, - select: { id: true, type: true, measuredAt: true }, - }); - createdRows.push(...rows); - createdCount += rows.length; - } - for (const chunk of chunked([...extUpdates.values()], WRITE_CHUNK)) { - await prisma.$transaction( - chunk.map((m) => - prisma.measurement.updateMany({ - where: { - userId, - source: "IMPORT", - type: m.type as MeasurementType, - externalId: m.externalId as string, - }, - data: { - value: m.value, - unit: m.unit, - measuredAt: m.measuredAt, - notes: null, - notesEncrypted: encryptNote(m.notes ?? null), - glucoseContext: - (m.glucoseContext as GlucoseContext | undefined) ?? null, - // No-op on a live row, a deliberate RESURRECTION on a - // tombstoned one — IMPORT rows are re-importable by design, so - // a re-imported externalId brings the row back (mirrors the - // source-owned sync resurrect rule). - deletedAt: null, - }, - }), - ), - ); - } - - void emitInsertedMeasurementArrivals( - userId, - createdRows, - "csv_import", - ).catch(() => {}); - void maybeEnqueueMorningRefresh( - userId, - createdRows - .filter((row) => row.type === "SLEEP_DURATION") - .map((row) => row.measuredAt), - ).catch(() => {}); - - // Reconcile `inserted` against what `createMany` actually wrote - // (mirrors the batch route's raced-duplicate downgrade). Under - // `skipDuplicates` a conflicting row is silently absorbed — the key is - // present in the table either way, so we cannot identify the SPECIFIC - // raced lines; we only need the counters and per-line statuses to sum - // to the truth. Downgrade enough `inserted` lines to `duplicate` so - // the envelope matches the DB write count. - const racedDuplicates = toCreate.length - createdCount; - if (racedDuplicates > 0) { - let downgraded = 0; - for (const [line, outcome] of writeOutcome) { - if (downgraded >= racedDuplicates) break; - if (outcome === "inserted") { - writeOutcome.set(line, "duplicate"); - inserted--; - skipped++; - downgraded++; - } - } - } - } finally { - // One bounded rollup re-fold per touched (type, day) — a 10 000-row - // CSV pays at most ~N (type, day) recomputes, not 10 000 per-row hooks - // — and the status re-warm for the touched types. Best-effort: a - // populator hiccup never fails the importer. - await afterMeasurementMutation(userId, touchedMeasurements, { - source: "manual", - label: "import.csv", - }); - } - - await auditLog("import.csv.upload", { - userId, + return apiSuccess( + await runCsvImport({ + userId: user.id, + rows: parsed.rows, + okRows, + dryRun, ipAddress: getClientIp(request), - details: { inserted, updated, skipped }, - }); - } else { - // Dry-run preview: no write ran, so report the projected outcome — - // every valid parse-`ok` row would insert (an externalId row could land - // as an update, but that needs a DB read we deliberately skip here). - inserted = okRows.length; - } - - const summary = { - inserted, - updated, - skipped, - total: parsed.rows.length, - dryRun, - // Per-row status, mirroring the batch route's per-entry envelope. The - // normalised payload is stripped — the client only needs line + status. - // Parse-time `ok` rows are refined by the write outcome: a row that hit a - // unique-constraint duplicate becomes `skipped`/`duplicate`; an upsert - // that landed on an existing row becomes `updated`. In `dryRun` mode no - // write ran, so every parse-`ok` row stays `inserted` (the projected - // outcome). - rows: parsed.rows.map((r) => { - if (r.status === "skipped") { - return { - line: r.line, - status: "skipped" as const, - ...(r.reason ? { reason: r.reason } : {}), - }; - } - const outcome = writeOutcome.get(r.line); - if (outcome === "duplicate") { - return { - line: r.line, - status: "skipped" as const, - reason: "duplicate", - }; - } - if (outcome === "updated") { - return { line: r.line, status: "updated" as const }; - } - return { line: r.line, status: "inserted" as const }; }), - }; - - annotate({ - meta: { - import_csv_inserted: inserted, - import_csv_updated: updated, - import_csv_skipped: skipped, - import_csv_dry_run: dryRun, - }, - }); - - return apiSuccess(summary); + ); }); diff --git a/src/app/api/import/route.ts b/src/app/api/import/route.ts index a1b46f9d1..8931865f0 100644 --- a/src/app/api/import/route.ts +++ b/src/app/api/import/route.ts @@ -1,7 +1,10 @@ -import { prisma } from "@/lib/db"; +/** + * `POST /api/import` — import user data (JSON format matching the export + * structure). The route authenticates, meters and parses the file; the + * writers live in `src/lib/import/json/`. + */ import { apiHandler, requireAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; -import { auditLog } from "@/lib/auth/audit"; import { apiError, apiSuccess, @@ -13,7 +16,6 @@ import { import { checkRateLimit } from "@/lib/rate-limit"; import { NextRequest } from "next/server"; import { z } from "zod/v4"; -import { createHash } from "node:crypto"; import { validateMeasurementRange, @@ -21,27 +23,12 @@ import { glucoseContextEnum, } from "@/lib/validations/measurement"; import { - ENTRY_INSTANT_CLOCK_SKEW_MS, isPlausibleEntryInstant, validateEntryInstant, } from "@/lib/validations/entry-instant"; -import { - classifyExternalId, - unstableExternalIdMeta, - type UnstableExternalIdShape, -} from "@/lib/validations/external-id"; -import { encryptNote } from "@/lib/crypto/note-cipher"; -import { recomputeUserMoodRollups } from "@/lib/rollups/mood-rollups"; -import { afterMeasurementMutation } from "@/lib/rollups/after-measurement-mutation"; -import type { MeasurementType } from "@/generated/prisma/client"; -import { emitInsertedMeasurementArrivals } from "@/lib/arrivals/measurement-emit"; -import { maybeEnqueueMorningRefresh } from "@/lib/daily/morning-refresh-trigger"; -import { isP2002, isP2025 } from "@/lib/prisma-errors"; -import { moodDateKey } from "@/lib/mood/date-key"; -import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; -import { deriveA1 } from "@/lib/mood/level-a"; -import { zonedWallClockToUtc } from "@/lib/tz/wall-clock"; import { isoDateTime } from "@/lib/validations/datetime"; +import { fallbackMoodDateStart } from "@/lib/import/json/mood"; +import { runJsonImport } from "@/lib/import/json/run"; // Derived from canonical enum so round-trip export → import covers every // type. Previous hardcoded subset silently dropped 4 of 11 types @@ -70,7 +57,7 @@ const measurementSchema = z // Stability is enforced per entry in the import loop, not at the // field: a schema-level refusal would fail the whole file on one bad // row. - // @external-id-checked-per-entry: src/app/api/import/route.ts + // @external-id-checked-per-entry: src/lib/import/json/measurements.ts externalId: z.string().min(1).max(120).optional(), }) .refine( @@ -78,16 +65,6 @@ const measurementSchema = z "Wert ausserhalb des plausiblen Bereichs", ); -function fallbackMoodDateStart(date: string): Date | null { - const [year, month, day] = date.split("-").map(Number); - if (!year || !month || !day) return null; - const instant = zonedWallClockToUtc( - { year, month, day, hour: 0, minute: 0 }, - DEFAULT_TIMEZONE, - ); - return moodDateKey(instant, DEFAULT_TIMEZONE) === date ? instant : null; -} - const moodEntrySchema = z .object({ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), @@ -106,7 +83,7 @@ const moodEntrySchema = z // Stability is enforced per entry in the import loop, not at the // field: a schema-level refusal would fail the whole file on one bad // row. - // @external-id-checked-per-entry: src/app/api/import/route.ts + // @external-id-checked-per-entry: src/lib/import/json/mood.ts externalId: z.string().min(1).max(120).optional(), }) .superRefine((entry, ctx) => { @@ -121,42 +98,11 @@ const moodEntrySchema = z } }); -type MoodEntryInput = z.infer; - -function fallbackMoodIdentity(entry: MoodEntryInput): string { - return JSON.stringify([ - entry.date, - entry.mood, - entry.score, - entry.tags ?? null, - entry.externalId ?? null, - ]); -} - -function stableFallbackMoodInstant( - entry: MoodEntryInput, - occurrence: number, - probe: number, -): Date { - const digest = createHash("sha256") - .update(`${fallbackMoodIdentity(entry)}:${occurrence}`) - .digest(); - const initialOffset = digest.readUInt32BE(0) % ENTRY_INSTANT_CLOCK_SKEW_MS; - const offset = (initialOffset + probe) % ENTRY_INSTANT_CLOCK_SKEW_MS; - const dateStart = fallbackMoodDateStart(entry.date); - if (!dateStart) throw new Error("Invalid mood date"); - return new Date(dateStart.getTime() + offset); -} - const importSchema = z.object({ measurements: z.array(measurementSchema).max(10000).optional(), moodEntries: z.array(moodEntrySchema).max(10000).optional(), }); -/** - * Import user data (JSON format matching export structure). - * Deduplicates via unique constraints — existing records are skipped. - */ export const POST = apiHandler(async (request: NextRequest) => { const { user } = await requireAuth(); annotate({ action: { name: "import.upload" } }); @@ -183,334 +129,13 @@ export const POST = apiHandler(async (request: NextRequest) => { ); } - const userId = user.id; - const data = parsed.data; - const stats = { measurements: 0, moodEntries: 0, skipped: 0 }; - const unstableShapes: UnstableExternalIdShape[] = []; - let writeFailed = false; - - // Import measurements - // v1.4.39.1 — track each (type, measuredAt) we wrote so we can re- - // fold the persistent rollup tier at the end. Pre-fix, the import - // path bypassed the rollup write hook entirely, so a CSV / JSON - // restore left the user's `source=rollup` chart paths under-counting - // the imported days until the next worker boot re-ran the backfill. - const touchedMeasurements: Array<{ - type: MeasurementType; - measuredAt: Date; - }> = []; - const insertedMeasurements: Array<{ - id: string; - type: MeasurementType; - measuredAt: Date; - }> = []; - if (data.measurements?.length) { - for (const m of data.measurements) { - // The `(userId, type, source, externalId)` upsert key is only - // idempotent while the id is STABLE; an id that rotates between - // exports re-imports as a fresh row every time. Skipped per entry, - // like every other rejected row in this importer. - const unstable = - m.externalId === undefined ? null : classifyExternalId(m.externalId); - if (unstable) { - unstableShapes.push(unstable); - stats.skipped++; - continue; - } - try { - // `measuredAt` is already a `Date` (validateEntryInstant transform). - const measuredAt = m.measuredAt; - if (m.externalId) { - const notesEncrypted = encryptNote(m.notes || null); - // INSERT ... RETURNING is the only insertion proof. A concurrent - // importer can win this key after any existence probe, so attempt - // the insert first and update only when the statement returns no row. - const created = await prisma.measurement.createManyAndReturn({ - data: [ - { - userId, - type: m.type, - value: m.value, - unit: m.unit, - source: "IMPORT", - externalId: m.externalId, - measuredAt, - notes: null, - notesEncrypted, - glucoseContext: m.glucoseContext ?? null, - }, - ], - skipDuplicates: true, - select: { id: true, type: true, measuredAt: true }, - }); - const inserted = created[0]; - if (inserted) { - insertedMeasurements.push(inserted); - } else { - // A pre-existing row or a raced insert owns the key. Preserve - // idempotent re-import semantics without classifying this update - // as an arrival. - try { - await prisma.measurement.update({ - where: { - userId_type_source_externalId: { - userId, - type: m.type, - source: "IMPORT", - externalId: m.externalId, - }, - }, - data: { - value: m.value, - unit: m.unit, - measuredAt, - notes: null, - notesEncrypted, - glucoseContext: m.glucoseContext ?? null, - }, - }); - } catch (err) { - if (isP2025(err)) { - stats.skipped++; - continue; - } - throw err; - } - } - } else { - const inserted = await prisma.measurement.create({ - data: { - userId, - type: m.type, - value: m.value, - unit: m.unit, - source: "IMPORT", - measuredAt, - notes: null, - notesEncrypted: encryptNote(m.notes || null), - glucoseContext: m.glucoseContext ?? null, - }, - select: { id: true, type: true, measuredAt: true }, - }); - insertedMeasurements.push(inserted); - } - touchedMeasurements.push({ - type: m.type as MeasurementType, - measuredAt, - }); - stats.measurements++; - } catch (err) { - if (isP2002(err)) { - stats.skipped++; - continue; - } - writeFailed = true; - break; - } - } - } - - void emitInsertedMeasurementArrivals( - userId, - insertedMeasurements, - "json_import", - ).catch(() => {}); - void maybeEnqueueMorningRefresh( - userId, - insertedMeasurements - .filter((row) => row.type === "SLEEP_DURATION") - .map((row) => row.measuredAt), - ).catch(() => {}); - - // Import mood entries - if (!writeFailed && data.moodEntries?.length) { - const fallbackOccurrences = new Map(); - const reservedFallbackInstants = new Set(); - for (const e of data.moodEntries) { - // Same stability floor as the measurement loop above — the mood - // upsert keys on `(userId, source, externalId)`. - const unstableMood = - e.externalId === undefined ? null : classifyExternalId(e.externalId); - if (unstableMood) { - unstableShapes.push(unstableMood); - stats.skipped++; - continue; - } - const identity = e.loggedAt ? null : fallbackMoodIdentity(e); - const occurrence = - identity === null ? 0 : (fallbackOccurrences.get(identity) ?? 0); - if (identity !== null) { - fallbackOccurrences.set(identity, occurrence + 1); - } - - for (let probe = 0; probe < ENTRY_INSTANT_CLOCK_SKEW_MS; probe++) { - const loggedAt = - e.loggedAt ?? stableFallbackMoodInstant(e, occurrence, probe); - if (!e.loggedAt && reservedFallbackInstants.has(loggedAt.getTime())) { - continue; - } - try { - if (e.externalId) { - // v1.12.1 — idempotent re-import keyed on the source-stable id. - // A second import of the same export updates the row in place - // rather than skipping it as a duplicate, so an upstream edit - // (re-scored mood, added tag) is reflected. - await prisma.moodEntry.upsert({ - where: { - userId_source_externalId: { - userId, - source: "IMPORT", - externalId: e.externalId, - }, - }, - update: { - date: e.date, - mood: e.mood, - score: e.score, - // v1.37 — pleasantness derived from the row's own label, not - // from the file's `score`. This is the one writer whose score - // comes out of the file rather than out of the label, so the - // two can disagree; the column that records a self-assessment - // has to agree with the label the user actually picked. - moodA1: deriveA1(e.mood), - tags: e.tags || null, - moodLoggedAt: loggedAt, - }, - create: { - userId, - date: e.date, - mood: e.mood, - score: e.score, - moodA1: deriveA1(e.mood), - tags: e.tags || null, - source: "IMPORT", - externalId: e.externalId, - moodLoggedAt: loggedAt, - }, - }); - } else { - await prisma.moodEntry.create({ - data: { - userId, - date: e.date, - mood: e.mood, - score: e.score, - // See the upsert arm above: derived from the label, because - // an imported file's `score` is its own field. - moodA1: deriveA1(e.mood), - tags: e.tags || null, - source: "IMPORT", - moodLoggedAt: loggedAt, - }, - }); - } - if (!e.loggedAt) { - reservedFallbackInstants.add(loggedAt.getTime()); - } - stats.moodEntries++; - break; - } catch (err) { - if (!isP2002(err)) { - writeFailed = true; - break; - } - if (e.loggedAt) { - stats.skipped++; - break; - } - if (!e.externalId) { - try { - const existing = await prisma.moodEntry.findUnique({ - where: { - userId_date_moodLoggedAt: { - userId, - date: e.date, - moodLoggedAt: loggedAt, - }, - }, - select: { - source: true, - mood: true, - score: true, - tags: true, - externalId: true, - }, - }); - if ( - existing?.source === "IMPORT" && - existing.mood === e.mood && - existing.score === e.score && - existing.tags === (e.tags || null) && - (existing.externalId ?? null) === null - ) { - reservedFallbackInstants.add(loggedAt.getTime()); - stats.skipped++; - break; - } - } catch { - writeFailed = true; - break; - } - } - if (probe === ENTRY_INSTANT_CLOCK_SKEW_MS - 1) { - writeFailed = true; - } - } - } - if (writeFailed) break; - } - } - - // v1.4.39 — one bounded re-fold per import is cheaper than - // firing N per-row hooks. The mood rollup tier is keyed on the - // user + day; the fold materialises every touched day from the - // entries we just wrote. Best-effort: an importer hiccup must not - // surface as a 5xx, the rollup is a cache tier. - if (stats.moodEntries > 0) { - try { - await recomputeUserMoodRollups(userId, { granularities: ["DAY"] }); - } catch (err) { - annotate({ - meta: { - mood_rollup_import_failed: true, - mood_rollup_import_error: - err instanceof Error ? err.message : String(err), - }, - }); - } - } - - // Refresh the persistent measurement rollup table for every distinct - // (type, day) the import touched (collapsed, so a 10 000-row restore pays - // at most ~N recomputes) and re-warm the touched types' status - // assessments. Best-effort: a populator hiccup never fails the importer. - await afterMeasurementMutation(userId, touchedMeasurements, { - // A file the person chose to import: a deliberate act, narrated like a - // hand entry rather than held to the sync gap. - source: "manual", - label: "import", + const { stats, writeFailed } = await runJsonImport({ + userId: user.id, + measurements: parsed.data.measurements, + moodEntries: parsed.data.moodEntries, + ipAddress: getClientIp(request), }); - annotate({ - meta: { - import_stats: stats, - ...(unstableShapes.length > 0 - ? unstableExternalIdMeta("import.json", unstableShapes) - : {}), - }, - }); - - try { - await auditLog("import.upload", { - userId, - ipAddress: getClientIp(request), - details: stats, - }); - } catch (err) { - if (!writeFailed) throw err; - annotate({ meta: { import_audit_failed: true } }); - } - if (writeFailed) { return apiError("Import write failed", 503, { errorCode: "import.write_failed", diff --git a/src/app/api/insights/biomarker-assessment/route.ts b/src/app/api/insights/biomarker-assessment/route.ts index 4297cfd26..e17b59c48 100644 --- a/src/app/api/insights/biomarker-assessment/route.ts +++ b/src/app/api/insights/biomarker-assessment/route.ts @@ -28,7 +28,7 @@ import { resolveServerLocale } from "@/lib/i18n/server-locale"; import { aiCapabilityToServe } from "@/lib/ai/capabilities/gate"; import { unavailableStatusBody } from "@/lib/insights/status-unavailable"; import { generateBiomarkerStatus } from "@/lib/insights/biomarker-status"; -import { normalizeLocale as resolveMetricStatusLocale } from "@/lib/insights/status-shared"; +import { normalizeLocale } from "@/lib/insights/status-shared"; export const dynamic = "force-dynamic"; @@ -67,7 +67,7 @@ export const GET = apiHandler(async (request: NextRequest) => { userLocale: user.locale ?? null, override: localeParam, }); - const locale = resolveMetricStatusLocale(resolved); + const locale = normalizeLocale(resolved); const result = await generateBiomarkerStatus({ biomarkerId: parsed.data.biomarkerId, diff --git a/src/app/api/insights/blood-pressure-status/route.ts b/src/app/api/insights/blood-pressure-status/route.ts index 2671d62fd..36d498d07 100644 --- a/src/app/api/insights/blood-pressure-status/route.ts +++ b/src/app/api/insights/blood-pressure-status/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from "next/server"; import { apiSuccess } from "@/lib/api-response"; import { generateBloodPressureStatusForUser } from "@/lib/insights/blood-pressure-status"; -import { normalizeLocale as resolveBloodPressureStatusLocale } from "@/lib/insights/status-shared"; +import { normalizeLocale } from "@/lib/insights/status-shared"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; @@ -40,7 +40,7 @@ export const GET = apiHandler(async (request: NextRequest) => { userLocale: user.locale ?? null, override: localeParam, }); - const locale = resolveBloodPressureStatusLocale(resolved); + const locale = normalizeLocale(resolved); // v1.8.3 — read-only: serve the cache, enqueue generation out of band on // a miss. The GET never awaits the provider, so opening /insights/ diff --git a/src/app/api/insights/bmi-status/route.ts b/src/app/api/insights/bmi-status/route.ts index afd28f1cf..3f42a2667 100644 --- a/src/app/api/insights/bmi-status/route.ts +++ b/src/app/api/insights/bmi-status/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from "next/server"; import { apiSuccess } from "@/lib/api-response"; import { generateBmiStatusForUser } from "@/lib/insights/bmi-status"; -import { normalizeLocale as resolveBmiStatusLocale } from "@/lib/insights/status-shared"; +import { normalizeLocale } from "@/lib/insights/status-shared"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; @@ -40,7 +40,7 @@ export const GET = apiHandler(async (request: NextRequest) => { userLocale: user.locale ?? null, override: localeParam, }); - const locale = resolveBmiStatusLocale(resolved); + const locale = normalizeLocale(resolved); // v1.8.3 — read-only: serve the cache, enqueue generation out of band on // a miss. The GET never awaits the provider, so opening /insights/ diff --git a/src/app/api/insights/comprehensive/route.ts b/src/app/api/insights/comprehensive/route.ts index a170b149c..bc3890f9c 100644 --- a/src/app/api/insights/comprehensive/route.ts +++ b/src/app/api/insights/comprehensive/route.ts @@ -1,59 +1,20 @@ -import { prisma } from "@/lib/db"; -import { probeProviderPresence } from "@/lib/ai/provider"; import { aiCapabilityToServe } from "@/lib/ai/capabilities/gate"; import { apiError, apiSuccess } from "@/lib/api-response"; -import type { DataPoint, DataSummary } from "@/lib/analytics/trends"; -import { summarize } from "@/lib/analytics/trends"; -import { getBpTargets } from "@/lib/analytics/bp-targets"; -import { computeBpInTargetPct } from "@/lib/analytics/bp-in-target"; -import { - classifyBMI, - classifyBP, - generateAlerts, -} from "@/lib/analytics/classifications"; -import { - significantPearsonCorrelation, - type PairedPoint, -} from "@/lib/analytics/correlations"; -import { - buildComplianceMedicationContext, - COURSES_COMPLIANCE_SELECT, - calculateCompliance, - expectsDoses, - lastNonSkippedTakenAt, - SCHEDULE_COMPLIANCE_SELECT, -} from "@/lib/analytics/compliance"; -import { resolveMedicationCategories } from "@/lib/medication-category"; -import { resolveRestingPulseSeries } from "@/lib/analytics/resting-pulse"; -import { readRestingPulseProxy } from "@/lib/analytics/resting-pulse-read"; -import { - apiHandler, - requireRecordAuth, - type AuthContext, -} from "@/lib/api-handler"; -import { annotate } from "@/lib/logging/context"; +import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { checkAnalyticsReadRateLimit } from "@/lib/rate-limit"; -import { - cachedSwrWithMeta, - caches, - type ServerCache, -} from "@/lib/cache/server-cache"; -import { buildComprehensiveAggregate } from "@/lib/insights/comprehensive-aggregator"; -import { - ensureUserMoodRollupsFresh, - readMoodDayRollups, -} from "@/lib/rollups/mood-rollups"; -import { userDayKey, DEFAULT_TIMEZONE } from "@/lib/tz/format"; -import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking"; -import { dayKeyAsUtcMidnight, dateOnlyKey } from "@/lib/tz/date-only"; +import { readComprehensiveOverview } from "@/lib/insights/comprehensive-overview/overview"; export const dynamic = "force-dynamic"; +/** + * GET /api/insights/comprehensive — the Insights overview's computed read. + * The body is built in `src/lib/insights/comprehensive-overview/`. + */ export const GET = apiHandler(async () => { - // v1.37.0 — MANAGE-level read: computed over the whole record, with no - // provider anywhere on the path. + // MANAGE-level read: computed over the whole record, with no provider + // anywhere on the path. const { user } = await requireRecordAuth("manage", "record"); - // v1.15.20 — shared analytics-read budget (generous; caps runaway loops). + // Shared analytics-read budget (generous; caps runaway loops). const rl = await checkAnalyticsReadRateLimit(user.id); if (!rl.allowed) { return apiError("Too many analytics requests. Please retry later.", 429); @@ -66,37 +27,7 @@ export const GET = apiHandler(async () => { // provider up, still gets the overview. The `ai` block beside the body says // whether the briefing and status notes that decorate the overview can be // shown, resolved per request and never cached with the body. - - // v1.4.35 — read-through the analytics cache keyed on - // (userId, "comprehensive"). The /insights page mount routinely - // fans out to this endpoint alongside the Coach drawer + the - // recommendations grid; the 60s TTL converts every duplicate - // mount within the window to a Map lookup. Invalidation is handled - // by `invalidateUserMeasurements`, which marks every key under the - // `${userId}|` prefix stale (see `lib/cache/invalidate.ts`). - // - // v1.16.7 — stale-while-revalidate. This is the single heaviest - // SQL-side aggregation on the /insights mount; serving the prior - // body instantly while ONE background recompute refreshes it keeps - // the page interactive across the bucket's 10-minute stale window - // (a measurement sync used to bust the entry into a blocking cold - // rebuild on the very next mount, all day). The truly-cold first - // read of the day still computes inline — there is no prior body to - // serve — but it no longer recurs per visit. - // - // The `revalidating` flag marks a stale-served body honestly so the - // client can poll (bounded) until the background rebuild lands — - // mirroring the `/api/insights/generate` GET contract. It rides on - // the response, never inside the cached body, so a warmed entry - // doesn't freeze the flag. - const { value: body, outcome } = await cachedSwrWithMeta( - caches.analytics as ServerCache< - Awaited> - >, - `${user.id}|comprehensive`, - () => buildComprehensiveResponse(user), - annotate, - ); + const { body, revalidating } = await readComprehensiveOverview(user); const [briefing, statusText] = await Promise.all([ aiCapabilityToServe(user.id, "briefing"), @@ -105,627 +36,7 @@ export const GET = apiHandler(async () => { return apiSuccess({ ...body, - revalidating: outcome === "stale", + revalidating, ai: { briefing, statusText }, }); }); - -type AuthedUser = AuthContext["user"]; - -/** - * v1.4.35 — comprehensive insights response body, lifted out of the - * route handler so `cached()` can wrap it. Replaces the previous - * 100k-row-in-JS aggregation with SQL-side aggregates served by - * `buildComprehensiveAggregate`. The medication-compliance block keeps - * its bounded Prisma reads (medications + intake events) — the directive - * called those out as already on the safe path. The byte-shape of the - * returned envelope is unchanged from the legacy route. - */ -export async function buildComprehensiveResponse(user: AuthedUser) { - const userId = user.id; - const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); - // v1.30.3 — the one shared per-user tz for every day-bucketing - // pass in this route (resting-pulse proxy, medication-continuity, BP - // pairing). Was previously re-derived ad hoc (or hardcoded UTC) at each - // call site. - const userTz = user.timezone || DEFAULT_TIMEZONE; - - // Fetch user profile (height + DOB drive BMI + BP targets). - const dbUser = await prisma.user.findUnique({ - where: { id: userId }, - select: { - heightCm: true, - dateOfBirth: true, - }, - }); - - // ── Aggregate-first reads ───────────────────────────────── - // SQL-side rollup replaces the 100k-row findMany on the legacy - // route. Returns per-type DataSummary identical to the JS - // `summarize()` semantics over the 90-day window, plus the bounded - // raw rows the BP target adherence pairing needs, plus daily-mean - // buckets for the correlation pairings. - const aggregate = await buildComprehensiveAggregate(userId); - - // v1.4.40 — swap the 90-day unbounded `prisma.moodEntry.findMany` - // for the persistent mood-rollup DAY tier (audit Critical Finding - // #2). The two consumers downstream — `dailyMoodEntries` for the - // mood × metric correlations and `moodSummary` for the dashboard - // mood block — read at a per-day resolution, which is precisely the - // shape the rollup writer emits. - // - // Coverage-fallback: when the user has mood entries but zero rollup - // rows yet (legacy account before the boot-time backfill has caught - // up), fall back to a bounded 90-day raw walk once and fire the - // warm-up so the next request lands on the rollup tier. Same posture - // as `/api/mood/analytics`. - // - // Since v1.32.12 the mood rollup writer mints `bucketStart` from the - // canonical local `MoodEntry.date` label at UTC midnight, so - // `.toISOString().slice(0, 10)` recovers the user's LOCAL calendar day — - // byte-identical to the live fallback's `entry.date`. Both mood paths - // therefore emit local-day keys. The mood × metric pairing partners - // below (sys BP, weight, resting pulse) are consequently derived in the - // user's tz (v1.32.17) so both join sides share ONE local-day key space. - // The measurement-rollup tier itself still buckets UTC (`dailyByType`) - // and migrates to local in a documented follow-up release; the two - // internally-consistent UTC joins (weight × BP, BP-med continuity) are - // left on it deliberately (see their call sites below). - void ensureUserMoodRollupsFresh(userId); - const moodRollupDayRows = await readMoodDayRollups(userId, ninetyDaysAgo); - let dailyMoodEntries: Array<{ day: string; value: number }>; - let moodDataPoints: DataPoint[]; - let moodEntryCount: number; - if (moodRollupDayRows.length > 0) { - dailyMoodEntries = moodRollupDayRows.map((r) => ({ - day: dateOnlyKey(r.bucketStart), - value: Math.round(r.mean * 100) / 100, - })); - moodDataPoints = moodRollupDayRows.map((r) => ({ - date: r.bucketStart, - value: r.mean, - })); - moodEntryCount = moodRollupDayRows.reduce((s, r) => s + r.count, 0); - } else { - // Coverage-fallback: legacy account with mood entries but no - // rollup coverage yet. Bounded by the 90-day window so the walk is - // capped even when the rollup miss happens. - const moodEntries = await prisma.moodEntry.findMany({ - // v1.7.0 sync — exclude tombstoned rows. - where: { userId, deletedAt: null, moodLoggedAt: { gte: ninetyDaysAgo } }, - orderBy: { moodLoggedAt: "asc" }, - select: { date: true, score: true, moodLoggedAt: true }, - }); - const moodByDay = new Map(); - for (const entry of moodEntries) { - const current = moodByDay.get(entry.date) ?? { sum: 0, count: 0 }; - current.sum += entry.score; - current.count += 1; - moodByDay.set(entry.date, current); - } - dailyMoodEntries = Array.from(moodByDay.entries()) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([day, stats]) => ({ - day, - value: Math.round((stats.sum / stats.count) * 100) / 100, - })); - // Feed per-day means into `summarize()` so the live-fallback path - // shares the rollup-tier semantic (one DataPoint per day, the - // daily mean) — mirrors the v1.4.39 mood/analytics QA UX-H1 fix. - moodDataPoints = dailyMoodEntries.map((e) => ({ - date: new Date(`${e.day}T12:00:00.000Z`), - value: e.value, - })); - moodEntryCount = moodEntries.length; - } - - const moodSummary = - moodDataPoints.length > 0 ? summarize(moodDataPoints) : null; - - const summaries: Record = aggregate.summaries; - - // ── BMI ────────────────────────────────────────────────── - let bmi: number | null = null; - let bmiClassification = null; - if (dbUser?.heightCm && summaries.WEIGHT?.latest) { - const heightM = dbUser.heightCm / 100; - bmi = Math.round((summaries.WEIGHT.latest / (heightM * heightM)) * 10) / 10; - bmiClassification = classifyBMI(bmi); - } - - // ── BP classification (30-day average) ──────────────────── - let bpClassification = null; - const bpTargets = getBpTargets(dbUser?.dateOfBirth ?? null); - if ( - summaries.BLOOD_PRESSURE_SYS?.avg30 && - summaries.BLOOD_PRESSURE_DIA?.avg30 - ) { - bpClassification = classifyBP( - summaries.BLOOD_PRESSURE_SYS.avg30, - summaries.BLOOD_PRESSURE_DIA.avg30, - ); - } - - // ── BP target adherence (trailing 90 days) ─────────────── - // Route through the canonical `computeBpInTargetPct` so this figure - // shares ONE definition with the dashboard tile, the insight cards, the - // AI snapshot and the targets endpoint (per the bp-in-target.ts - // contract — the inline copy that used to live here drifted: it paired - // sys+dia on the 5-minute window ONLY, dropping readings whose sys/dia - // timestamps drift past 5 min on Withings / Apple-Health imports, which - // skewed the share iOS renders on the Home Health Score). The helper - // adds the same-local-day pairing fallback (v1.30.3 QA F7 — keyed on - // the user's OWN tz, not a hardcoded Berlin day) so those readings - // pair, and keeps the ESH-2023 band + 90/50 hypotension floor (both sys - // AND dia at or below the age-band ceiling). Window is the aggregator's - // 90-day raw pull — a "recent control" figure, not the all-time headline. - let bpPctInTarget: number | null = null; - if (bpTargets) { - bpPctInTarget = - computeBpInTargetPct( - aggregate.bpRawRows.sys, - aggregate.bpRawRows.dia, - bpTargets, - userTz, - )?.pct ?? null; - } - - // ── Correlations: weight × Sys BP ───────────────────────── - // Daily-key pairing (the legacy `pairByTimestamp` ran with a - // 24-hour default tolerance — daily-mean buckets are the - // SQL-side equivalent and the directive explicitly accepts the - // small semantic shift to bound the raw-row footprint). - // - // v1.32.17 — this pair stays on the UTC `dailyByType` series: BOTH - // sides come from `dailyByType` (weight × sys), so they share one UTC - // day-key space and are internally consistent. Re-keying it to local - // is R12's read-swap (it migrates with the rollup tier), NOT this - // hotfix — doing it here would mean per-request raw re-aggregation - // without the local-bucketed rollup behind it. - const weightDaily = aggregate.dailyByType.WEIGHT ?? []; - const sysDaily = aggregate.dailyByType.BLOOD_PRESSURE_SYS ?? []; - const weightBpPairs: PairedPoint[] = joinDailyByDay(weightDaily, sysDaily); - const weightBpCorrelation = significantPearsonCorrelation(weightBpPairs); - const scatterData = weightBpPairs.map((p) => ({ - weight: p.a, - sysBP: p.b, - })); - - // ── Correlations: mood × {sys BP, weight, pulse} ───────── - // v1.32.17 — the mood series is LOCAL-day-keyed (since v1.32.12, see the - // rollup note above). `pairMoodWithDaily` joins on an EXACT YYYY-MM-DD - // key, so the metric partners MUST also be local-day-keyed or every - // evening logger west of UTC gets a lag-1 pairing sold as same-day. - // Derive the sys / weight partners as exact local-day means from the raw - // rows already on the request path (canonical-source deduped in the - // aggregator), NOT from the UTC `dailyByType` — a UTC-day MEAN cannot be - // relabeled into a local-day mean. `round2` mirrors the aggregator's - // `ROUND(AVG, 2)` so correlation thresholds don't drift. - const sysDailyLocal = deriveDailyLocalMeans(aggregate.bpRawRows.sys, userTz); - const weightDailyLocal = deriveDailyLocalMeans( - aggregate.weightRawRows, - userTz, - ); - - const moodBpPairs = pairMoodWithDaily(dailyMoodEntries, sysDailyLocal); - const moodBpCorrelation = significantPearsonCorrelation(moodBpPairs); - const moodBpScatterData = moodBpPairs.map((p) => ({ mood: p.a, sysBP: p.b })); - - const moodWeightPairs = pairMoodWithDaily(dailyMoodEntries, weightDailyLocal); - const moodWeightCorrelation = significantPearsonCorrelation(moodWeightPairs); - const moodWeightScatterData = moodWeightPairs.map((p) => ({ - mood: p.a, - weight: p.b, - })); - - // v1.2.5 — this correlation is labelled "Resting pulse (bpm)"; - // score it against the RESTING series, not the raw PULSE day-mean. The - // aggregator's `dailyByType.PULSE` is a per-day AVG that folds in workout - // HR for Apple-Health users and inflates the "resting" figure. Prefer the - // clean RESTING_HEART_RATE rows; only pull RAW PULSE (for the - // low-percentile daily proxy) when the user has no resting rows — the - // proxy needs the per-day distribution, which the day-mean discards. - // - // v1.32.17 — key this resting series on `userDayKey(d, userTz)`. Its ONLY - // pairing partner is `dailyMoodEntries`, which is LOCAL-day-keyed since - // v1.32.12 (the mood rollup writer now mints `bucketStart` from the - // canonical local `MoodEntry.date` label, not a UTC truncation). Keeping - // UTC here would offset every evening logger west of UTC by one day and - // sell a lag-1 correlation as same-day. This is exactly the flip the - // v1.30.3 F8 note said becomes correct once the mood side goes local — - // it now has. (Unlike the mood side, this series builds from raw rows in - // this route, so it re-keys directly with no rollup-tier change.) - const restingPulseRows = await prisma.measurement.findMany({ - where: { - userId, - type: "RESTING_HEART_RATE", - deletedAt: null, - measuredAt: { gte: ninetyDaysAgo }, - }, - select: { measuredAt: true, value: true }, - }); - // - // #1023 — the proxy folds in Postgres. It needs one percentile per day, and - // reading the raw samples to get it walked 130 000 rows per page view on a - // watch that records once a minute, blocking the event loop for over a - // second. `readRestingPulseProxy` returns the same series one row per day. - const localDayKey = (d: Date) => userDayKey(d, userTz); - const restingPulseSeries = - restingPulseRows.length > 0 - ? resolveRestingPulseSeries({ - restingSamples: restingPulseRows, - pulseSamples: [], - dayKeyOf: localDayKey, - }).series - : await readRestingPulseProxy({ - userId, - since: ninetyDaysAgo, - timeZone: userTz, - }); - const restingByDay = new Map(); - for (const s of restingPulseSeries) { - const key = localDayKey(s.measuredAt); - const cur = restingByDay.get(key) ?? { sum: 0, count: 0 }; - cur.sum += s.value; - cur.count += 1; - restingByDay.set(key, cur); - } - const restingPulseDaily = Array.from(restingByDay.entries()) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([day, stats]) => ({ day, value: stats.sum / stats.count })); - const moodPulsePairs = pairMoodWithDaily(dailyMoodEntries, restingPulseDaily); - const moodPulseCorrelation = significantPearsonCorrelation(moodPulsePairs); - const moodPulseScatterData = moodPulsePairs.map((p) => ({ - mood: p.a, - pulse: p.b, - })); - - // ── Medication compliance ──────────────────────────────── - // Bounded reads — left untouched per the directive. - const medicationRows = await prisma.medication.findMany({ - // v1.16.11 — as-needed (PRN) medications never surface a compliance - // rate (no expected doses). - where: { userId, active: true, asNeeded: false, ...TRACKED_INTAKE_WHERE }, - // v1.15.20 — schedules through the shared compliance select so the - // configured per-dose windows reach this surface like every other. - include: { - schedules: { select: SCHEDULE_COMPLIANCE_SELECT }, - // v1.16.3 — archived schedule eras for era-aware compliance. - scheduleRevisions: { orderBy: { validFrom: "asc" } }, - // v1.25 — pause eras so paused days drop out of the denominator. - pauseEras: { select: { pausedAt: true, resumedAt: true } }, - // v1.40 (#1024) — the courses, so a gap between two expects nothing. - courses: COURSES_COMPLIANCE_SELECT, - }, - }); - // A medication with no schedule expects no dose, and `calculateCompliance` - // answers 100 % for that case. The `asNeeded: false` filter above catches - // only the PRN arm; the empty schedule is reachable on a scheduled - // medication and would otherwise report perfect adherence. - const medications = medicationRows.filter(expectsDoses); - - const resolvedCategories = await resolveMedicationCategories( - medications.map((m) => m.id), - ); - const categoryMap = Object.fromEntries( - Object.entries(resolvedCategories).map(([id, r]) => [id, r.category]), - ); - - const medCompliance = []; - const bpMedicationEvents: Array<{ - scheduledFor: Date; - takenAt: Date | null; - skipped: boolean; - }> = []; - const bpMedications = medications.filter( - (med) => (categoryMap[med.id] ?? "OTHER") === "BLOOD_PRESSURE", - ); - // Single round-trip for all medications instead of N+1: one query keyed - // on `medicationId IN (...)`, then group in memory. - const allEvents = medications.length - ? await prisma.medicationIntakeEvent.findMany({ - where: { - medicationId: { in: medications.map((m) => m.id) }, - userId, - // v1.7.0 sync — exclude tombstoned rows. - deletedAt: null, - scheduledFor: { gte: ninetyDaysAgo }, - }, - orderBy: { scheduledFor: "desc" }, - }) - : []; - - const eventsByMed = new Map(); - for (const ev of allEvents) { - const list = eventsByMed.get(ev.medicationId); - if (list) list.push(ev); - else eventsByMed.set(ev.medicationId, [ev]); - } - - for (const med of medications) { - const events = eventsByMed.get(med.id) ?? []; - const mapped = events.map((e) => ({ - takenAt: e.takenAt, - skipped: e.skipped, - scheduledFor: e.scheduledFor, - })); - // v1.7.0 — engine-routed denominator. - const medicationContext = buildComplianceMedicationContext( - med, - lastNonSkippedTakenAt(mapped), - user.timezone || DEFAULT_TIMEZONE, - ); - const c7 = calculateCompliance(mapped, med.schedules, 7, med.createdAt, { - medicationContext, - }); - const c30 = calculateCompliance(mapped, med.schedules, 30, med.createdAt, { - medicationContext, - }); - - medCompliance.push({ - id: med.id, - name: med.name, - dose: med.dose, - category: categoryMap[med.id] ?? "OTHER", - // v1.40 (#1041) — the label of a custom category; null for a built-in. - categoryLabel: resolvedCategories[med.id]?.categoryLabel ?? null, - compliance7: c7.rate, - compliance30: c30.rate, - streak: c7.streak, - taken7: c7.taken, - skipped7: c7.skipped, - missed7: c7.missed, - }); - - if ((categoryMap[med.id] ?? "OTHER") === "BLOOD_PRESSURE") { - bpMedicationEvents.push( - ...events.map((e) => ({ - scheduledFor: e.scheduledFor, - takenAt: e.takenAt, - skipped: e.skipped, - })), - ); - } - } - - // ── Correlation: BP medication continuity × systolic BP ── - // Uses the same daily BP_SYS series the aggregator pre-computed — - // no second round-trip. The intake-side aggregation matches the - // legacy code: continuity = min(1, taken / expectedPerDay), pairing - // every day that had a sys reading. - let bpMedicationCorrelation: { - r: number; - strength: string; - n: number; - medicationCount: number; - } | null = null; - const bpMedicationScatterData: Array<{ - continuityPct: number; - sysBP: number; - }> = []; - - const expectedBpIntakesPerDay = bpMedications.reduce( - (sum, med) => sum + med.schedules.length, - 0, - ); - - if (expectedBpIntakesPerDay > 0) { - // Daily systolic means (already keyed YYYY-MM-DD) from the SQL pass. - // v1.32.17 — `sysDaily` (UTC `date_trunc` from the aggregator's - // `dailyByType`) and `takenByDay` below (intake `scheduledFor` - // truncated to UTC) both live in the SAME UTC day-key space and are - // internally consistent with each other — this join is NOT broken and - // stays as is. Only the mood pairings above moved to a separately - // derived local-day series (their partner, the mood series, is - // local-day-keyed). This correlation migrates to local when the - // measurement-rollup tier buckets per-user tz (R12), not in this hotfix - // — re-keying it here without the local-bucketed rollup would mean - // per-request raw re-aggregation. - const sysByDay = new Map(); - for (const row of sysDaily) { - sysByDay.set(row.day, row.value); - } - - const takenByDay = new Map(); - // Days that carry an intake record of ANY kind. A logged skip is a real - // zero and belongs in the series; a day the person never touched is not a - // zero, it is nothing, and pairing it against that day's systolic reading - // invented a data point they never produced. Absence is not zero, here as - // everywhere else in this project, so the unlogged day drops out of both - // the scatter and the coefficient rather than dragging continuity down. - const loggedDays = new Set(); - for (const event of bpMedicationEvents) { - // eslint-disable-next-line healthlog/no-utc-day-key -- UTC by design: pairs intake days with the DAY rollup buckets in sysByDay, which are UTC days - const dayKey = event.scheduledFor.toISOString().slice(0, 10); - loggedDays.add(dayKey); - if (event.skipped || !event.takenAt) continue; - takenByDay.set(dayKey, (takenByDay.get(dayKey) ?? 0) + 1); - } - - const pairs: Array<{ a: number; b: number; date: Date }> = []; - for (const [dayKey, avgSys] of sysByDay.entries()) { - if (!loggedDays.has(dayKey)) continue; - const taken = takenByDay.get(dayKey) ?? 0; - const continuity = Math.min(1, taken / expectedBpIntakesPerDay); - pairs.push({ - a: continuity, - b: avgSys, - date: dayKeyAsUtcMidnight(dayKey), - }); - bpMedicationScatterData.push({ - continuityPct: Math.round(continuity * 100), - sysBP: Math.round(avgSys * 10) / 10, - }); - } - - const corr = significantPearsonCorrelation(pairs); - if (corr) { - bpMedicationCorrelation = { - ...corr, - medicationCount: bpMedications.length, - }; - } - } - - // Generate alerts - const alerts = generateAlerts({ - bmi, - bpAvgSys: summaries.BLOOD_PRESSURE_SYS?.avg30 ?? null, - bpAvgDia: summaries.BLOOD_PRESSURE_DIA?.avg30 ?? null, - bpPctInTarget, - weightSlope30: summaries.WEIGHT?.slope30?.slope ?? null, - pulseAvg30: summaries.PULSE?.avg30 ?? null, - pulseAnomalyCount: summaries.PULSE?.anomalyCount ?? 0, - medications: medCompliance.map((m) => ({ - name: m.name, - compliance7: m.compliance7, - compliance30: m.compliance30, - })), - }); - - // Data span — derived from the earliest 90-day measurement. - const dataSpanDays = aggregate.firstMeasurementAt - ? Math.ceil( - (Date.now() - aggregate.firstMeasurementAt.getTime()) / - (24 * 60 * 60 * 1000), - ) - : 0; - - annotate({ - action: { name: "insights.comprehensive" }, - meta: { - totalMeasurements: aggregate.totalMeasurements, - moodEntries: moodEntryCount, - medications: medications.length, - }, - }); - - return { - summaries, - bmi, - bmiClassification, - bpClassification, - bpPctInTarget, - bpTargets, - weightBpCorrelation, - scatterData, - bpMedicationCorrelation, - bpMedicationScatterData, - moodSummary, - moodBpCorrelation, - moodBpScatterData, - moodWeightCorrelation, - moodWeightScatterData, - moodPulseCorrelation, - moodPulseScatterData, - medications: medCompliance, - alerts, - // Presence only: no client is built and no token is refreshed on a read. - hasProvider: await probeProviderPresence(userId), - dataSpanDays, - totalMeasurements: aggregate.totalMeasurements, - }; -} - -// ───────────────────────────────────────────────────────────── -// Helpers -// ───────────────────────────────────────────────────────────── - -/** - * v1.32.17 — derive exact local-day mean series from raw measurement rows, - * keyed by the user's timezone via `userDayKey`. The mood series is - * local-day-keyed (since v1.32.12), and `pairMoodWithDaily` joins on an - * exact YYYY-MM-DD key, so its metric partners must be local-day-keyed too. - * A UTC-day mean (from `dailyByType`) cannot be relabeled into a local-day - * mean, so the mean is recomputed here from raw rows already on the request - * path. `userDayKey` formats via a memoised `Intl.DateTimeFormat` — no - * offset arithmetic — so DST-transition days (23 h / 25 h) bucket correctly - * and the result is host-TZ-independent (CI runs UTC). `round2` mirrors the - * aggregator's `ROUND(AVG, 2)` so correlation thresholds don't drift. - * Returns `{ day, value }[]` sorted ASC by day, the shape - * `pairMoodWithDaily` expects. - */ -function deriveDailyLocalMeans( - rows: Array<{ measuredAt: Date; value: number }>, - tz: string, -): Array<{ day: string; value: number }> { - const byDay = new Map(); - for (const r of rows) { - const key = userDayKey(r.measuredAt, tz); - const cur = byDay.get(key) ?? { sum: 0, count: 0 }; - cur.sum += r.value; - cur.count += 1; - byDay.set(key, cur); - } - return Array.from(byDay.entries()) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([day, stats]) => ({ - day, - value: Math.round((stats.sum / stats.count) * 100) / 100, - })); -} - -/** - * Daily-key inner join between two daily-aggregated series. Returns - * `PairedPoint` shapes compatible with `significantPearsonCorrelation`. - * - * Inputs are sorted ASC by `day` (the aggregator's ORDER BY - * guarantees that). The walk is O(n + m) merge-join style instead of - * Map.lookup-per-element so it scales with the worst-case 90 buckets - * x 90 buckets case without surprises. - */ -function joinDailyByDay( - a: Array<{ day: string; value: number }>, - b: Array<{ day: string; value: number }>, -): PairedPoint[] { - const pairs: PairedPoint[] = []; - let i = 0; - let j = 0; - while (i < a.length && j < b.length) { - const ai = a[i]; - const bj = b[j]; - if (ai.day === bj.day) { - pairs.push({ - a: ai.value, - b: bj.value, - date: new Date(`${ai.day}T12:00:00.000Z`), - }); - i += 1; - j += 1; - } else if (ai.day < bj.day) { - i += 1; - } else { - j += 1; - } - } - return pairs; -} - -/** - * Pair the daily mood series (left side) with a daily-aggregated - * metric (right side). Matches the legacy `buildMoodMetricPairs` - * helper byte-for-byte: same daily YYYY-MM-DD key, same - * `T12:00:00.000Z` anchor for the `date` field. - */ -function pairMoodWithDaily( - dailyMood: Array<{ day: string; value: number }>, - dailyMetric: Array<{ day: string; value: number }>, -): PairedPoint[] { - if (dailyMood.length === 0 || dailyMetric.length === 0) return []; - const metricByDay = new Map(); - for (const m of dailyMetric) { - metricByDay.set(m.day, m.value); - } - const pairs: PairedPoint[] = []; - for (const mood of dailyMood) { - const metric = metricByDay.get(mood.day); - if (metric !== undefined) { - pairs.push({ - a: mood.value, - b: metric, - date: new Date(`${mood.day}T12:00:00.000Z`), - }); - } - } - return pairs; -} diff --git a/src/app/api/insights/generate/route.ts b/src/app/api/insights/generate/route.ts index 394be24bb..3884229d2 100644 --- a/src/app/api/insights/generate/route.ts +++ b/src/app/api/insights/generate/route.ts @@ -1,4 +1,8 @@ /** + * GET /api/insights/generate — the read-only advisor briefing; POST — an + * on-demand generation. Both live in `src/lib/insights/briefing-request/`; + * this file keeps the gates, the body, the hourly quota and the statuses. + * * Refs #786 — async-truth contract, server end (the client end lives in * `src/components/insights/use-insights-advisor.ts`, `AdvisorSettleState`): * the force POST generates INLINE and, on success, writes the fresh briefing @@ -9,131 +13,44 @@ * GET until `cachedAt` advances past its baseline (success) or a NEWER * failure marker appears (honest error). Renaming or short-circuiting either * write breaks that convergence — the settling seam tests pin the client - * half, this comment is the server half of the contract. + * half, this comment and `briefing-request/commit.ts` the server half. */ -import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; -import { readBriefingGeneratedAt } from "@/lib/insights/briefing-generated-at"; -import { - generatedOnLocalToday, - insightsPayloadForToday, -} from "@/lib/daily/briefing-today-read"; -import { withBriefingGeneratedAt } from "@/lib/insights/briefing-generated-at"; -import { prisma } from "@/lib/db"; -import { auditLog } from "@/lib/auth/audit"; +import { NextRequest } from "next/server"; + import { apiSuccess, apiError, getClientIp, safeJson, } from "@/lib/api-response"; -import { - extractFeatures, - FeaturesPayloadTooLargeError, - BRIEFING_FEATURE_WINDOW_DAYS, -} from "@/lib/insights/features"; -import { applyInsightsExcludeFilter } from "@/lib/insights/exclude-filter"; -import { - getCachedFeatures, - withFeatureCacheScope, -} from "@/lib/insights/feature-cache"; -import { - buildBriefingIllnessCycleContext, - buildBriefingIllnessCyclePrompt, -} from "@/lib/insights/illness-cycle-briefing"; -import { compactSections } from "@/lib/ai/prompts/compact-sections"; -import { - detectGlp1Plateau, - buildGlp1PlateauPrompt, -} from "@/lib/insights/glp1-plateau"; -import { - detectDerivedBriefingSignals, - buildDerivedBriefingPrompt, -} from "@/lib/insights/derived-briefing"; -import { getAgeFromDateOfBirth } from "@/lib/analytics/pulse-targets"; -import { buildUserPrompt } from "@/lib/ai/prompts/insight-system-prompt"; -import { - buildSystemPromptWithReferences, - buildBriefingPersonalisationBlock, -} from "@/lib/ai/prompts/insight-generator"; -import { - buildAboutMeInsightBlock, - getSelfContextTextForUser, -} from "@/lib/ai/coach/about-me"; -import { metricsFromPresentSections } from "@/lib/ai/medical-references"; -import { resolveUserTimezone } from "@/lib/tz/resolver"; -import { insightResultSchema, type InsightResult } from "@/lib/ai/types"; -import { AI_BUDGETS, resolveInsightsMaxTokens } from "@/lib/ai/ai-budgets"; -import { resolveEffectiveTimeoutMs } from "@/lib/ai/effective-timeout"; -import { providerFailureKind } from "@/lib/ai/failure-kind"; -import { - recordBriefingFailure, - readBriefingFailure, -} from "@/lib/insights/briefing-failure-marker"; -import { resolveProvider, resolveProviderChain } from "@/lib/ai/provider"; -import { AllProvidersFailedError } from "@/lib/ai/provider-runner"; -import { - BriefingBudgetExceededError, - runBriefingCompletion, -} from "@/lib/insights/briefing-provider"; -import { assertConsentForChain } from "@/lib/ai/consent-guard"; -import { screenInsightPayloadProse } from "@/lib/ai/safety/insight-payload-screen"; -import { - findUngroundedBriefingNumbers, - readBriefingBlock, - buildBriefingGroundingCorrection, -} from "@/lib/ai/briefing-grounding"; -import { isLegacyInsightPayload } from "@/lib/ai/legacy-payload"; -import { checkRateLimit } from "@/lib/rate-limit"; -import { NextRequest } from "next/server"; import { apiHandler, requireAuth } from "@/lib/api-handler"; import { requireAiCapability } from "@/lib/ai/capabilities/gate"; import { AI_PROVIDER_NONE_ERROR_CODE } from "@/lib/ai/capabilities/types"; -import { invalidateUserInsights } from "@/lib/cache/invalidate"; -import { annotate } from "@/lib/logging/context"; -import { toProfileSex } from "@/lib/profile/sex"; -import { resolveServerLocale } from "@/lib/i18n/server-locale"; -import { normalizeLocale } from "@/lib/insights/status-shared"; -import { hasUsableStatusProvider } from "@/lib/insights/status-provider"; -import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash"; +import { withFeatureCacheScope } from "@/lib/insights/feature-cache"; import { - applyBriefingShortForm, - briefingFormForRecord, - buildBriefingShortFormBlock, -} from "@/lib/insights/briefing-short-form"; -import { featuresInReaderUnits } from "@/lib/insights/features-units"; -import { resolveUnitPreferences } from "@/lib/measurements/display-transform"; -import { enqueueForceWarm } from "@/lib/jobs/insight-pregenerate-shared"; -// v1.28.25 — the comparison-snapshot builder was a private near-copy of -// the lib export (drifted only in comments); the route now shares the -// one implementation the nightly warm pass uses. + briefingFromCache, + serveCachedFallback, +} from "@/lib/insights/briefing-request/cache"; import { - buildComparisonSnapshotForUser, - enqueueStatusRefillForUser, -} from "@/lib/insights/comprehensive-generate"; -import { cachedPayloadCarriesBriefing } from "@/lib/insights/briefing-payload"; + loadGenerationContext, + type BriefingRefusal, +} from "@/lib/insights/briefing-request/context"; import { - INSIGHTS_CACHE_SELECT, - insightsCacheWrite, - openInsightsCachedText, -} from "@/lib/insights/cached-text-cipher"; + generateBriefing, + resolveConsentedBriefingChain, +} from "@/lib/insights/briefing-request/generate"; +import { readAdvisorBriefing } from "@/lib/insights/briefing-request/read"; +import { checkRateLimit } from "@/lib/rate-limit"; export const dynamic = "force-dynamic"; -/** - * Briefing freshness window for the read-only GET. A cached briefing read - * this recently is fresh and triggers no warm; older / missing enqueues an - * out-of-band regeneration. Mirrors the 24 h cache-hit window the POST path - * honours so the two stay consistent. - */ -const BRIEFING_FRESH_MS = 24 * 60 * 60 * 1000; - const DEFAULT_INSIGHTS_RATE_LIMIT_PER_HOUR = 10; /** * Reads `INSIGHTS_RATE_LIMIT_PER_HOUR` from the environment with a - * sensible 10/hour default. The previous hard-coded 2/hour limit (from - * v1.4 P13) was too aggressive for users iterating on settings or - * regenerating after adding measurements. Operators who run on a + * sensible 10/hour default. A hard-coded 2/hour limit was too aggressive + * for users iterating on settings or regenerating after adding + * measurements. Operators who run on a * stricter LLM budget can dial it down via env without redeploying * code; values <1 fall back to the default. */ @@ -147,18 +64,81 @@ export function resolveInsightsRateLimit(): number { return parsed; } +const PROVIDER_REJECTED = + "AI provider rejected the request. Check your API key in Settings > AI"; +const PROVIDER_UNAVAILABLE = + "AI provider temporarily unavailable, try again in a moment"; + /** - * GET /api/insights/generate — read-only advisor read. - * - * The advisor consumers (hero strip, daily-briefing card, trends row) used - * to mount against the POST handler, which generates inline on a cache miss - * and blocks the page-load path on the full provider chain (up to the - * client's 8 s abort). This read-only GET serves the cached payload from - * `User.insightsCachedTextEncrypted` immediately — NEVER calling the provider — and, - * when the cache is stale / missing AND a provider is configured, enqueues - * an out-of-band warm (the same `insight-pregenerate` queue the nightly - * cron and the "prepare assessments" button use). The next read reflects - * the fresh briefing. User-initiated regeneration stays on the POST path. + * The answer for each way a generation ends without a briefing. A 502 from + * a provider would be rewritten by Cloudflare into an HTML page the client + * cannot parse, so provider failures answer 422 / 429 / 503 instead. + */ +const REFUSALS: Record< + BriefingRefusal, + { message: string; status: number; errorCode?: string } +> = { + "payload-too-large": { + message: + "Your data exceeds the AI payload size limit. Reduce the data window or exclude optional blocks in Settings > AI.", + status: 422, + }, + // The day's token ceiling: no provider was contacted. The hourly rate + // limit bounds request frequency; this bounds cost, with its own code so + // the client can say so. + "budget-exceeded": { + message: + "Daily AI token budget reached. Insights will be available again tomorrow.", + status: 429, + errorCode: "insights.generate.budgetExceeded", + }, + "provider-rejected": { message: PROVIDER_REJECTED, status: 422 }, + "provider-rate-limited": { + message: "AI provider rate-limited the request", + status: 429, + }, + "provider-unavailable": { message: PROVIDER_UNAVAILABLE, status: 503 }, + "provider-connection": { + message: "AI provider connection failed. Check your AI settings", + status: 422, + }, + "response-truncated": { + message: + "AI response was cut off before the JSON completed. Raise the token limit (INSIGHTS_MAX_TOKENS)", + status: 422, + errorCode: "ai_response_truncated", + }, + "response-invalid-json": { + message: "AI response was not valid JSON", + status: 422, + }, + "outbound-screened": { + message: + "The generated insight did not pass the safety check and was discarded. Try regenerating.", + status: 502, + errorCode: "insights.generate.outboundScreened", + }, + "profile-scope-changed": { + message: "Profile scope changed during insight generation", + status: 409, + errorCode: "insights.generate.profileScopeChanged", + }, + "scope-changed": { + message: "Insight scope changed during generation", + status: 409, + errorCode: "insights.generate.scopeChanged", + }, +}; + +function refuse(refusal: BriefingRefusal) { + const { message, status, errorCode } = REFUSALS[refusal]; + return errorCode + ? apiError(message, status, { errorCode }) + : apiError(message, status); +} + +/** + * GET — read-only advisor read: the cached payload, never a provider call. * * A pure AI read: the payload is the model-written briefing and nothing * else, so it keeps a typed refusal rather than answering 200 with nothing. @@ -172,183 +152,23 @@ export function resolveInsightsRateLimit(): number { */ export const GET = apiHandler(async (request: NextRequest) => { const { user } = await requireAuth(); - // The briefing is its own capability now, not the Coach's: a user with the + // The briefing is its own capability, not the Coach's: a user with the // Coach hidden still reads their briefing, and one who opted out of AI // analysis does not. await requireAiCapability("briefing"); - const userId = user.id; - - const dbUser = await prisma.user.findUnique({ - where: { id: userId }, - select: { - insightsCachedAt: true, - ...INSIGHTS_CACHE_SELECT, - insightsCachedLocale: true, - locale: true, - timezone: true, - }, - }); - const cachedText = openInsightsCachedText(dbUser); - - // Resolve the locale the caller is actually reading (cookie / - // Accept-Language fall-back when `User.locale` is unset) and validate it - // against the shipped list — the same convention the nightly's - // `normalizeLocale` and the status routes follow. The previous - // `(locale) === "en" ? "en" : "de"` collapsed a NULL `User.locale` - // to German even for a client reading the app in English, so the - // visit-triggered warm filled the wrong cache family. - const readerLocale = normalizeLocale( - await resolveServerLocale({ + return apiSuccess( + await readAdvisorBriefing({ request, - userLocale: dbUser?.locale ?? user.locale ?? null, + userId: user.id, + userLocale: user.locale ?? null, }), ); - - // The cache is one slot per user. Text written in another language is - // never the reader's briefing, however fresh: treat the slot as empty for - // prose, and re-warm in the reader's language below. An untagged row - // predates the tag and is served as before. - const languageMismatch = - dbUser?.insightsCachedLocale != null && - dbUser.insightsCachedLocale !== readerLocale; - - const cachedAt = dbUser?.insightsCachedAt ?? null; - const timezone = dbUser?.timezone ?? DEFAULT_TIMEZONE; - // Fresh means written today on the reader's calendar, by the moment the - // text was generated: `insightsCachedAt` also moves when an unchanged - // warm only re-stamps yesterday's text. - const generatedToday = generatedOnLocalToday( - readBriefingGeneratedAt(cachedText), - timezone, - new Date(), - ); - const isFresh = - !languageMismatch && - cachedAt !== null && - generatedToday && - Date.now() - cachedAt.getTime() < BRIEFING_FRESH_MS; - - // v1.18.9 (#4) — resolve provider availability once, up front. The - // read path never blocks on or generates through the provider, but it - // must REPORT whether one exists: a provider-less account's cached - // briefing freezes at the last successful generation (the pregenerate - // cron can't refill it), and the UI presented that stale text as - // current. Surfacing `hasProvider: false` lets the insights surfaces - // pair the honest "Stand: vor X Tagen" age with a discreet - // connect-an-AI-provider hint instead of implying the read is live. - const hasProvider = await hasUsableStatusProvider(userId); - - // v1.25 — surface whether the most recent generation attempt failed (a - // marker newer than the last successful generation). The briefing keeps its - // last good text on failure, so this is the only honest signal that a shown - // briefing is held rather than freshly refreshed — and, when there is no - // last good text, that the empty state should read "couldn't generate" with - // a retry rather than the generic "no briefing yet". - const briefingFailure = await readBriefingFailure({ - userId, - since: cachedAt, - }); - // v1.28.30 — a `briefing-ungrounded` marker is NOT a failure: the - // generation succeeded but the grounding gate withheld the briefing - // (marker written after the cache write, so it survives the `since` - // filter). Surface it as the persistent omission signal — the card's - // "briefing withheld" state used to exist only on the transient POST - // response, so a nightly strip read as a silent generic empty card. - const briefingOmittedReason: "ungrounded" | null = - briefingFailure?.reason === "briefing-ungrounded" ? "ungrounded" : null; - const generationFailed = - briefingFailure !== null && briefingOmittedReason === null; - // v1.25.3 — the failure class lets the empty state point its hint at the - // right lever (raise the response timeout vs re-check the provider). - const generationFailureClass = generationFailed - ? (briefingFailure?.failureClass ?? null) - : null; - - // Read-only: never block on the provider. Warm out of band only when the - // cached briefing is stale / missing AND a provider is configured (a - // provider-less account costs one cheap chain-resolve and shows the - // empty / connect-AI state instead of a wasted enqueue). - let revalidating = false; - if (!isFresh && hasProvider) { - void enqueueForceWarm({ userId, locale: readerLocale }); - revalidating = true; - } - - if (cachedText && !languageMismatch) { - try { - const cached = JSON.parse(cachedText); - const legacyPayload = isLegacyInsightPayload(cached); - annotate({ - action: { name: "insights.generate.read" }, - meta: { cached: true, legacyPayload, revalidating, hasProvider }, - }); - return apiSuccess({ - // The briefing as every surface serves it: today's text only, its - // signals measured today, its deltas at display precision. - insights: await insightsPayloadForToday(cached, { - userId, - timezone, - language: readerLocale, - }), - cached: true, - cachedAt, - legacyPayload, - // Honest stale-serve marker: true while the out-of-band warm is - // in flight, so the client can poll (bounded) until the fresh - // briefing lands instead of sitting on the stale payload for the - // rest of the session. - revalidating, - // v1.18.9 (#4) — false when no AI provider is configured anywhere: - // the served briefing can never refresh, so the UI pairs its age - // with a connect-provider affordance. - hasProvider, - // v1.25 — true when the last generation attempt failed (held text). - generationFailed, - // v1.25.3 — coarse failure class for the empty-state hint (null when - // the last attempt succeeded). - generationFailureClass, - // v1.28.30 — persistent grounding-omission signal (see above), so a - // nightly-stripped briefing renders "withheld", not "no briefing yet". - briefingOmittedReason, - }); - } catch { - // Invalid cache row — fall through to the empty payload below. The - // warm enqueue above (when a provider exists) repairs it for the - // next read. - } - } - - annotate({ - action: { name: "insights.generate.read" }, - meta: { - cached: false, - revalidating, - hasProvider, - languageMismatch, - readerLocale, - }, - }); - return apiSuccess({ - insights: null, - cached: false, - legacyPayload: false, - revalidating, - // v1.18.9 (#4) — see the cached branch above. - hasProvider, - // v1.25 — no last-good text AND the last attempt failed: the UI shows a - // "couldn't generate" empty state with a retry instead of the generic one. - generationFailed, - // v1.25.3 — coarse failure class for the empty-state hint. - generationFailureClass, - // v1.28.30 — see the cached branch above. - briefingOmittedReason, - }); }); export const POST = apiHandler((request: NextRequest) => - // v1.18.11 — open a request-scoped feature cache so the bounded feature - // read is computed once and reused across the downgrade ladder (and any - // sibling consumer) for this request. + // A request-scoped feature cache, so the bounded feature read is computed + // once and reused across the downgrade ladder (and any sibling consumer) + // for this request. withFeatureCacheScope(async () => { const { user } = await requireAuth(); // The `briefing` capability, before any read or provider work. Refused @@ -357,63 +177,11 @@ export const POST = apiHandler((request: NextRequest) => await requireAiCapability("briefing"); const userId = user.id; - const dbUser = await prisma.user.findUnique({ - where: { id: userId }, - select: { - insightsPrivacyMode: true, - insightsCachedAt: true, - ...INSIGHTS_CACHE_SELECT, - insightsCachedLocale: true, - timezone: true, - // v1.4.36 — per-user opt-out list mirroring the Coach - // settings. Filtered off `features` before serialisation so the - // LLM never sees the excluded blocks. - insightsExcludeMetrics: true, - locale: true, - // v1.10.0 — profile for the derived-signal briefing detector. - dateOfBirth: true, - gender: true, - heightCm: true, - // v1.22 — first name for the sparse, hash-gated briefing opener. - displayName: true, - // v1.25 — per-user response-timeout (seconds), threaded onto the - // generation calls so a slow self-hosted backend honours the setting. - aiResponseTimeoutSeconds: true, - // The reader's units; the feature set is converted into them before - // it is serialised, hashed and checked for grounding. - unitPreference: true, - glucoseUnit: true, - }, - }); - const cachedText = openInsightsCachedText(dbUser); - const modeAtRead = dbUser?.insightsPrivacyMode ?? "aggregated"; - // v1.34 — the commit guard below (line ~966) matches on these exact - // columns instead of the broad `updatedAt` timestamp. Every User write - // bumps `updatedAt`, including `resolveProviderChain()`'s OAuth - // token-refresh update later in this same request — keying the - // optimistic-concurrency check on it produced a false 409 whenever a - // provider credential happened to refresh mid-generation. These are - // exactly the columns selected above that feed the prompt built below; - // capturing them here and requiring an exact match at commit still - // catches a genuine privacy-mode / exclude-list / locale / profile - // change without colliding on unrelated column writes. - const excludeMetricsAtRead = dbUser?.insightsExcludeMetrics ?? []; - const localeAtRead = dbUser?.locale ?? null; - const dateOfBirthAtRead = dbUser?.dateOfBirth ?? null; - const genderAtRead = dbUser?.gender ?? null; - const heightCmAtRead = dbUser?.heightCm ?? null; - const displayNameAtRead = dbUser?.displayName ?? null; - - const effectiveTimeoutMs = resolveEffectiveTimeoutMs( - dbUser?.aiResponseTimeoutSeconds, - AI_BUDGETS.comprehensive.timeoutMs, - ); - - const locale = await resolveServerLocale({ + const ctx = await loadGenerationContext({ request, - userLocale: dbUser?.locale ?? user.locale ?? null, + userId, + userLocale: user.locale ?? null, }); - const postTimezone = dbUser?.timezone ?? DEFAULT_TIMEZONE; const { data: body, error: jsonError } = await safeJson<{ force?: unknown; @@ -421,97 +189,12 @@ export const POST = apiHandler((request: NextRequest) => if (jsonError) return jsonError; const forceRefresh = body.force === true; - // v1.28.30 — a fresh cache WITHOUT a briefing must not satisfy a - // briefing-expecting caller: after a failed nightly warm the cached - // payload can sit inside the 24 h window all day, and the old - // unconditional short-circuit served it to every POST (three - // regenerate attempts, three `cached: true` responses, zero - // generations). The short-circuit now holds only when the cached - // payload actually carries a briefing OR the account has no usable - // provider (regenerating would be futile — serve what exists). The - // read-only GET is untouched: it never generates and its out-of-band - // revalidation behaviour stays as-is. When the briefingless fall- - // through is rate-limited below, the cached payload is served after - // all so a POST-as-read client degrades to the old behaviour instead - // of a 429. - let briefinglessCacheFallback: { - cached: unknown; - legacyPayload: boolean; - cachedAt: Date; - } | null = null; - // A cache tagged with another language is not this reader's cache and - // never short-circuits; the generation below writes the reader's own. - const cachedLocaleMatches = - dbUser?.insightsCachedLocale == null || - dbUser.insightsCachedLocale === locale; - if ( - !forceRefresh && - dbUser?.insightsCachedAt && - cachedText && - cachedLocaleMatches && - Date.now() - dbUser.insightsCachedAt.getTime() < 24 * 60 * 60 * 1000 - ) { - try { - const cached = JSON.parse(cachedText); - // v1.4.16: detect legacy payloads (pre-rationale) so the UI - // can show a "regenerate for new explainability features" CTA. - // We don't auto-regenerate — that would burn rate-limit tokens - // on a cache-hit silently. User-initiated only. - const legacyPayload = isLegacyInsightPayload(cached); - // A briefing generated on an earlier day is not today's read: like - // a briefingless cache, it does not short-circuit a provider-backed - // account, and degrades to the cached payload on a 429 below. - const carriesBriefing = - cachedPayloadCarriesBriefing(cachedText) && - generatedOnLocalToday( - readBriefingGeneratedAt(cachedText), - postTimezone, - new Date(), - ); - if (carriesBriefing || !(await hasUsableStatusProvider(userId))) { - annotate({ - action: { name: "insights.generate" }, - meta: { - cached: true, - legacyPayload, - briefingless: !carriesBriefing, - }, - }); - return apiSuccess({ - insights: await insightsPayloadForToday(cached, { - userId, - timezone: postTimezone, - language: locale, - }), - cached: true, - cachedAt: dbUser.insightsCachedAt, - legacyPayload, - }); - } - briefinglessCacheFallback = { - cached, - legacyPayload, - cachedAt: dbUser.insightsCachedAt, - }; - annotate({ - action: { name: "insights.generate.briefingless_cache_bypassed" }, - meta: { locale }, - }); - } catch { - // Invalid cache, regenerate - } - } + const decision = await briefingFromCache(ctx, forceRefresh); + if (decision.kind === "serve") return apiSuccess(decision.data); - // P13: rate-limit check moved BELOW the cache return so a hit on the - // 24h cache never burns one of the LLM-generation tokens. Otherwise a - // noisy reload loop on the dashboard would lock out real refreshes - // for an hour for no benefit. - // - // v1.4.16: limit raised from 2 → 10 per hour (env-configurable via - // `INSIGHTS_RATE_LIMIT_PER_HOUR`). The 2/hour limit triggered too - // aggressively when a user iterated on settings or regenerated after - // adding a few measurements; 10/hour still cleanly bounds cost while - // staying out of the way for legitimate use. + // The rate limit sits BELOW the cache return so a hit on the 24 h cache + // never burns one of the generation tokens; otherwise a noisy reload + // loop on the dashboard would lock out real refreshes for an hour. const limit = resolveInsightsRateLimit(); const rl = await checkRateLimit( `insights:${userId}`, @@ -519,696 +202,32 @@ export const POST = apiHandler((request: NextRequest) => 60 * 60 * 1000, ); if (!rl.allowed) { - // v1.28.30 — a POST-as-read caller whose fresh-but-briefingless cache - // fell through to regeneration degrades to the cached payload when - // the hourly quota is exhausted, instead of surfacing a 429 it never - // used to see. Explicit `force` keeps the honest 429. - if (briefinglessCacheFallback !== null) { - annotate({ - action: { name: "insights.generate" }, - meta: { cached: true, rate_limited_fallback: true }, - }); - return apiSuccess({ - insights: await insightsPayloadForToday( - briefinglessCacheFallback.cached, - { userId, timezone: postTimezone, language: locale }, - ), - cached: true, - cachedAt: briefinglessCacheFallback.cachedAt, - legacyPayload: briefinglessCacheFallback.legacyPayload, - }); + // A POST-as-read caller whose fresh-but-briefingless cache fell + // through to regeneration degrades to the cached payload when the + // hourly quota is exhausted, instead of surfacing a 429 it never used + // to see. Explicit `force` keeps the honest 429. + if (decision.fallback !== null) { + return apiSuccess(await serveCachedFallback(ctx, decision.fallback)); } return apiError(`Maximum ${limit} insight generations per hour.`, 429); } - // v1.4.16: resolve the user's full provider chain so a hard - // failure on the primary (401, 5xx, network) cascades to the next - // configured fallback rather than surfacing a 503/422 to the - // dashboard. Falls back to the single-provider resolution when the - // chain comes back empty AND the legacy `aiProvider` field still - // points at something usable — preserving v1.4.15 behaviour for - // accounts that haven't customised the chain. - const chain = await resolveProviderChain(userId); - if (chain.length === 0) { - // Try the legacy single-provider path one more time so users who - // configured AI before v1.4.16 don't suddenly see the 422. - const legacy = await resolveProvider(userId); - if (legacy.type === "none") { - return apiError( - "No AI provider configured. Connect ChatGPT in settings or ask your admin to set up an API key.", - 422, - { errorCode: AI_PROVIDER_NONE_ERROR_CODE }, - ); - } - chain.push({ providerType: "admin-openai", instance: legacy }); - } - - // v1.12.1 — consent gate before any server-managed external egress. - // Required for the operator's global key (`admin-openai`); BYOK / local / - // ChatGPT-OAuth chains stay ungated. Throws ConsentRequiredError → - // apiHandler returns 403 + `consent.ai.required`. - await assertConsentForChain({ userId, chain, surface: "insights" }); - - const includeRaw = dbUser?.insightsPrivacyMode === "raw"; - // v1.4.36 — `extractFeatures` enforces a 5 MB ceiling on the - // serialised payload. If the raw (bucketed) shape ever blows past - // it (regression watch — the v1.4.35 rawMeasurements shape hit - // 25.9 MB on the largest tenant), the helper throws - // `FeaturesPayloadTooLargeError`. We downgrade to the aggregated - // shape rather than 500-ing so the user still gets an insight; the - // annotate event surfaces the regression to ops via the Logflare - // pipeline. - let features: Awaited>; - let payloadDowngraded = false; - let payloadOversizeBytes: number | null = null; - // v1.4.36 — extra fallback when even the aggregated shape - // crosses the 5 MB ceiling (very large medication history + multi- - // year context). Strips anthropometrics + medications + every other - // exclude-token-mapped block and tries one more time. On a third - // failure we return 422 with an annotate event for ops visibility - // rather than 500-ing out of an uncaught throw. - const MAX_DOWNGRADE_TOKENS: ReadonlyArray = [ - "anthropometrics", - "medications", - "compliance", - "sleep", - "steps", - "hrv", - "resting_hr", - ]; - let payloadHardDowngraded = false; - // v1.18.11 — bound the briefing's bulk feature read to a recent window - // (all-time extremes are sourced separately, see features.ts). The downgrade - // ladder below stays as the safety net for the rare oversize payload. - const featureWindow = { sinceDays: BRIEFING_FEATURE_WINDOW_DAYS }; - try { - // v1.18.11 — compute-once-per-request: the scope opened around this - // handler shares the bounded feature read across the downgrade ladder. - features = await getCachedFeatures({ - userId, - includeRaw, - sinceDays: featureWindow.sinceDays, - compute: () => extractFeatures(userId, includeRaw, featureWindow), - }); - } catch (err) { - if (err instanceof FeaturesPayloadTooLargeError) { - payloadDowngraded = true; - payloadOversizeBytes = err.sizeBytes; - try { - features = await getCachedFeatures({ - userId, - includeRaw: false, - sinceDays: featureWindow.sinceDays, - compute: () => extractFeatures(userId, false, featureWindow), - }); - } catch (retryErr) { - if (retryErr instanceof FeaturesPayloadTooLargeError) { - // Aggregated shape ALSO crossed the ceiling. Drop optional - // context blocks (anthropometrics, medications, sleep, - // steps, hrv, resting_hr) via the existing exclude filter - // and try once more. - payloadHardDowngraded = true; - payloadOversizeBytes = retryErr.sizeBytes; - try { - const aggregated = await getCachedFeatures({ - userId, - includeRaw: false, - sinceDays: featureWindow.sinceDays, - compute: () => extractFeatures(userId, false, featureWindow), - }); - features = applyInsightsExcludeFilter( - aggregated, - MAX_DOWNGRADE_TOKENS, - ); - } catch (finalErr) { - // Even the aggregated read itself blew up. Annotate and - // return 422 rather than let the uncaught throw 500 out. - annotate({ - meta: { - insights_payload_too_large: true, - insights_payload_oversize_bytes: - finalErr instanceof FeaturesPayloadTooLargeError - ? finalErr.sizeBytes - : payloadOversizeBytes, - }, - }); - return apiError( - "Your data exceeds the AI payload size limit. Reduce the data window or exclude optional blocks in Settings > AI.", - 422, - ); - } - } else { - throw retryErr; - } - } - } else { - throw err; - } - } - // v1.4.36 — apply the user's exclude-metrics opt-out list - // BEFORE serialisation so the model never sees the dropped blocks. - const excludeList = dbUser?.insightsExcludeMetrics ?? []; - features = applyInsightsExcludeFilter(features, excludeList); - // The reader's units, after all canonical arithmetic: the prompt, the - // stored hash and the grounding check read the same converted figures. - const units = resolveUnitPreferences({ - unitPreference: dbUser?.unitPreference, - glucoseUnit: dbUser?.glucoseUnit, - }); - features = featuresInReaderUnits(features, units); - // v1.4.36 — drop zero-row blocks so the prompt never carries - // labelled-empty sections (`"sleep": []`, `"medications": []`, - // etc.). Prevents the model from narrating "there are no - // medications in your data" when the user explicitly excluded the - // block. - const compactFeatures = compactSections( - features as unknown as Record, - ); - // Compact on the wire: indentation is about 40% of a pretty-printed - // payload, and the model reads the structure without it (#1217). - const featuresJson = JSON.stringify(compactFeatures); - if (payloadDowngraded) { - annotate({ - meta: { - insights_features_downgraded: true, - insights_features_oversize_bytes: payloadOversizeBytes, - insights_features_hard_downgraded: payloadHardDowngraded, - }, - }); - } - - // v1.4.16 — pick up the user's persisted comparison toggle - // and, when active, build a compact prior-period snapshot from the - // `features` summaries that already include `avg30LastMonth` / - // `avg30LastYear` (added in the analytics summarize() helper). The - // narrative is default-on whenever the toggle is on per research §7 - // Q4 — the pulldown is the single affordance. - const comparisonSnapshot = await buildComparisonSnapshotForUser(userId); - // v1.4.25 — GLP-1 plateau detector. Appends a SYSTEM CONTEXT - // block to the user prompt when the active GLP-1 medication has been - // on a stable dose for ≥21 days with weight delta within ±0.5 kg. - // The block instructs the model to emit a glp1_plateau keyFinding - // framed observationally (no dose recommendation per GROUND RULE 13). - // Returns null when no GLP-1 is active OR no plateau condition; - // 99% of users land in the null branch and pay zero token cost. - const plateauContext = await detectGlp1Plateau(userId); - let userPrompt = buildUserPrompt( - featuresJson, - dbUser?.insightsPrivacyMode ?? "aggregated", - locale, - comparisonSnapshot ?? undefined, - ); - if (plateauContext) { - userPrompt += buildGlp1PlateauPrompt( - plateauContext, - locale, - units.system, - ); - } - // v1.15.20 — fold the user-authored "about me" self-description - // (Settings → AI) into the briefing as a delimited, user-provided - // SYSTEM CONTEXT block. Null (no text / undecryptable) costs nothing. - const aboutMe = await getSelfContextTextForUser(userId, locale); - if (aboutMe) { - userPrompt += buildAboutMeInsightBlock(aboutMe, locale); - } - // v1.10.0 — fold a notable derived wellness signal (readiness / recovery - // shift, confidence-gated) into the briefing as a SYSTEM CONTEXT block. - // Reads the one derived contract; null (the common case) costs nothing. - const derivedSex = toProfileSex(dbUser?.gender); - const derivedBriefing = await detectDerivedBriefingSignals(userId, { - ageYears: getAgeFromDateOfBirth(dbUser?.dateOfBirth ?? null), - sex: derivedSex, - heightCm: dbUser?.heightCm ?? null, - }); - if (derivedBriefing) { - userPrompt += buildDerivedBriefingPrompt(derivedBriefing, locale); - } - - // v1.18.11 — fold illness-episode + cycle state into the briefing so a - // mid-illness or mid-cycle user gets context-aware prose. Reuses the same - // server-authoritative builders the Coach assembles; both are module-gated - // and short-circuit to null (zero token cost) for users without them. - const illnessCycleCtx = await buildBriefingIllnessCycleContext( - userId, - dbUser?.gender ?? null, - await resolveUserTimezone(userId), - ); - if (illnessCycleCtx) { - userPrompt += buildBriefingIllnessCyclePrompt(illnessCycleCtx, locale); - } - - // v1.22 — opener-archetype rotation + sparse first-name personalization - // (deterministic per user+day; omitted for unnamed accounts). - userPrompt += buildBriefingPersonalisationBlock( - userId, - dbUser?.displayName ?? null, - locale, - await resolveUserTimezone(userId), - ); - // v1.43 (#1217) — the short form on the `frugal` usage level; the full - // contract on every other level. - const briefingForm = await briefingFormForRecord(userId); - userPrompt += buildBriefingShortFormBlock(locale, briefingForm); - - // v1.12.7 — inject the curated SOURCES block for the metric sections - // this briefing actually carries, so a normative claim can cite a real - // `referenceId` the schema + UI footnote already support. Returns the plain - // prompt unchanged when no applicable metric section is present. - const referenceMetrics = metricsFromPresentSections({ - bloodPressure: features.bloodPressure != null, - weight: features.weight != null, - pulse: features.pulse != null, - mood: features.mood != null, - medication: (features.medications?.length ?? 0) > 0, - }); - - let result; - let workingProviderType: string; - let fallbackHopCount = 0; - try { - const fallback = await runBriefingCompletion({ - userId, - chain, - // The strict prompt (PROMPT_VERSION 4.20.x) carries GROUND RULE 8 - // — emit a top-level `dailyBriefing` block when the snapshot has - // analysable signal — plus the trendAnnotations and - // storyboardAnnotations rules. The legacy `getInsightsSystemPrompt` - // returned the v1.4.5 `{changed, stable, drivers, …}` shape and - // never asked the model for dailyBriefing, so the hero strip's - // "Re-run analysis" button kept producing a payload with no - // briefing block (Issue 1, v1.4.20 post-deploy). The route already - // tolerates the strict shape via `insightResultSchema.safeParse`'s - // soft fallback to `parsed` and `passthrough()` on the strict - // schema, so switching the prompt does not break legacy callers. - systemPrompt: buildSystemPromptWithReferences(locale, referenceMetrics), - userPrompt, - temperature: 0.3, - // v1.28.28 (#470) — env-tunable output ceiling (INSIGHTS_MAX_TOKENS, - // default 2500). The old fixed 1500 truncated verbose models' - // briefing JSON mid-string → generic invalid-JSON 422. - maxTokens: resolveInsightsMaxTokens(), - // v1.21.5 — wider upstream budget so the reasoning-heavy briefing - // generation is not aborted at the client's 60 s default on large - // accounts (which returned an empty briefing + trend narrative). - // v1.25 — honours the per-user response-timeout setting. - timeoutMs: effectiveTimeoutMs, - stage: "generate", - // v1.41 — a briefing asked for by hand reasons exactly like the - // scheduled one; the retries below repair a reply and do not. - reasoningJob: "daily_briefing", - }); - result = fallback.result; - workingProviderType = fallback.workingProvider.providerType; - fallbackHopCount = fallback.fallbackHops.length; - } catch (e) { - // The day's token ceiling refused the reservation — no provider was - // contacted, so this is NOT a provider failure and must not record a - // briefing-failure marker (which would make the read path claim the - // refresh broke). The hourly rate limit bounds request frequency; this - // bounds cost. 429 with a distinct code so the client can say so. - if (e instanceof BriefingBudgetExceededError) { - return apiError( - "Daily AI token budget reached. Insights will be available again tomorrow.", - 429, - { errorCode: "insights.generate.budgetExceeded" }, - ); - } - // v1.25 — record a dated failure marker (no cache row is written on this - // path, so the last good briefing stays intact). The read path pairs the - // preserved text with a discreet "couldn't refresh" hint, or shows a - // retry empty state when there is no last good text. - // v1.25.3 — carry the upstream status so the read-path hint can tell an - // auth / rate-limit failure apart from a plain timeout. For an - // all-providers-failed error the first hop's status is the most - // representative; a single provider error exposes its own `httpStatus`. - const failureHttpStatus = - e instanceof AllProvidersFailedError - ? (e.attempts[0]?.httpStatus ?? null) - : typeof (e as { httpStatus?: number }).httpStatus === "number" - ? (e as { httpStatus?: number }).httpStatus - : null; - void recordBriefingFailure({ - userId, - reason: - e instanceof AllProvidersFailedError - ? "all-providers-failed" - : "provider-error", - locale, - httpStatus: failureHttpStatus, - failureKind: - e instanceof AllProvidersFailedError - ? (e.attempts[0]?.failureKind ?? null) - : providerFailureKind(e).failure, - }); - if (e instanceof AllProvidersFailedError) { - annotate({ - meta: { - insights_chain_outcome: "all-failed", - insights_chain_attempts: e.attempts.length, - insights_chain_first_provider: e.attempts[0]?.providerType ?? null, - insights_chain_first_status: e.attempts[0]?.httpStatus ?? null, - }, - }); - // Pick the most specific user-facing message we can. Auth-class - // failures across the entire chain mean every provider's - // credential is bad — keep the v1.4.5 "check your API key" - // wording so the user knows where to act. 429 across the chain - // is rate-limit; 5xx is upstream brown-out; transport-only - // (ECONNRESET, no httpStatus) maps to the generic connection - // hint so users on flaky networks don't see "rate-limited". - const allAuth = e.attempts.every( - (a) => a.httpStatus === 401 || a.httpStatus === 403, - ); - if (allAuth) { - return apiError( - "AI provider rejected the request. Check your API key in Settings > AI", - 422, - ); - } - const all429 = e.attempts.every((a) => a.httpStatus === 429); - if (all429) { - return apiError("AI provider rate-limited the request", 429); - } - const has5xx = e.attempts.some( - (a) => a.httpStatus !== null && a.httpStatus >= 500, - ); - if (has5xx) { - return apiError( - "AI provider temporarily unavailable, try again in a moment", - 503, - ); - } - const allTransport = e.attempts.every( - (a) => a.httpStatus === null || a.httpStatus <= 0, - ); - if (allTransport) { - return apiError( - "AI provider connection failed. Check your AI settings", - 422, - ); - } - return apiError( - "AI provider temporarily unavailable, try again in a moment", - 503, - ); - } - // v1.4.6 T5 mapped only the parse-error branch from 502→422. Non- - // hard provider errors (e.g. a custom 4xx from a self-hosted local - // model) still propagate here. Cloudflare rewrites 5xx to its own - // HTML error page, which breaks `await res.json()` on the - // dashboard. Mirror the v1.4.5 ai/test categorisation. - const err = e as Error & { httpStatus?: number; bodyExcerpt?: string }; - annotate({ - meta: { - insights_provider_error: err.message.slice(0, 500), - insights_provider_status: err.httpStatus ?? null, - insights_provider_body_excerpt: - err.bodyExcerpt?.slice(0, 500) ?? null, - }, - }); - const status = err.httpStatus ?? 0; - if (status === 401 || status === 403) { - return apiError( - "AI provider rejected the request. Check your API key in Settings > AI", - 422, - ); - } - if (status === 429) { - return apiError("AI provider rate-limited the request", 429); - } - if (status >= 500) { - return apiError( - "AI provider temporarily unavailable, try again in a moment", - 503, - ); - } + // The chain, with the consent gate on it (403 `consent.ai.required` + // before any server-managed external egress). + const chain = await resolveConsentedBriefingChain(userId); + if (chain === null) { return apiError( - "AI provider connection failed. Check your AI settings", + "No AI provider configured. Connect ChatGPT in settings or ask your admin to set up an API key.", 422, + { errorCode: AI_PROVIDER_NONE_ERROR_CODE }, ); } - let insights: InsightResult | Record; - try { - const parsed = JSON.parse(result.content); - // Try new schema first, fall back to raw parsed if validation fails - const validated = insightResultSchema.safeParse(parsed); - insights = validated.success ? validated.data : parsed; - } catch { - // Returning 502 here triggers Cloudflare's HTML error rewrite, which - // breaks `await res.json()` on the client side. 422 stays passthrough - // so the React Query mutation can read the JSON body and surface a - // readable message. Same fix pattern as v1.4.5 ai/test. - void recordBriefingFailure({ userId, reason: "invalid-json", locale }); - // v1.28.28 (#470) — truncation-aware: `finishReason === "length"` means - // the model hit the output-token ceiling and the JSON was cut off - // mid-string, which is a budget problem, not a model-quality one. Say - // so with a distinct errorCode instead of the generic invalid-JSON line. - if (result.finishReason === "length") { - annotate({ meta: { insights_response_truncated: true } }); - return apiError( - "AI response was cut off before the JSON completed. Raise the token limit (INSIGHTS_MAX_TOKENS)", - 422, - { errorCode: "ai_response_truncated" }, - ); - } - return apiError("AI response was not valid JSON", 422); - } - - // v1.18.10 — daily-briefing number-grounding gate, mirroring the - // recommendations citation cross-check next door. The schema only checks the - // briefing's SHAPE; this asserts every number the briefing prose restates - // traces to a `features.signalsOfDay` figure. On a miss: one corrective - // retry, then drop the briefing block rather than ship a fabricated number. - // - // v1.28.28 (#470) — when the gate strips the briefing, SAY SO: the 200 - // used to carry a silently-null briefing, the card showed "no briefing - // yet", and the regenerate button read as doing nothing. The additive - // `briefingOmittedReason` field lets the card render an honest "a figure - // couldn't be verified, so the briefing wasn't shown" state instead. - let briefingOmittedReason: "ungrounded" | null = null; - { - const signals = features.signalsOfDay ?? null; - let ungrounded = findUngroundedBriefingNumbers( - readBriefingBlock(insights), - signals, - features, - comparisonSnapshot, - ); - if (ungrounded.length > 0) { - annotate({ - action: { name: "insights.generate.briefing_grounding_retry" }, - meta: { ungroundedCount: ungrounded.length }, - }); - try { - const retry = await runBriefingCompletion({ - userId, - chain, - systemPrompt: buildSystemPromptWithReferences( - locale, - referenceMetrics, - ), - userPrompt: `${userPrompt}\n\n${buildBriefingGroundingCorrection(ungrounded)}`, - temperature: 0.3, - // v1.28.28 (#470) — same env-tunable ceiling as the first call. - maxTokens: resolveInsightsMaxTokens(), - // v1.21.5 — wider upstream budget; see the first generation call. - // v1.25 — honours the per-user response-timeout setting. - timeoutMs: effectiveTimeoutMs, - stage: "grounding-retry", - }); - const parsedRetry = JSON.parse(retry.result.content); - const validatedRetry = insightResultSchema.safeParse(parsedRetry); - const retryInsights = validatedRetry.success - ? validatedRetry.data - : parsedRetry; - ungrounded = findUngroundedBriefingNumbers( - readBriefingBlock(retryInsights), - signals, - features, - comparisonSnapshot, - ); - if (ungrounded.length === 0) { - insights = retryInsights; - result = retry.result; - workingProviderType = retry.workingProvider.providerType; - } - } catch { - // Retry failed to produce parseable JSON, or the day's ceiling - // refused the correction pass — either way fall through to the - // strip. An ungrounded figure is never persisted. - } - } - // Still ungrounded after the retry: the freshly generated briefing is - // discarded rather than shipped — the anti-fabrication guarantee is - // absolute here too. But rather than leave a hole, stand the PREVIOUS - // cached briefing in its place: it passed this identical gate when it - // was written, so it carries no fabricated figure, and a day-old - // grounded briefing beats a vanished one. This mirrors the background - // generator, which is the path that writes the same cached payload; - // the two must not disagree on disposal. - // - // `briefingOmittedReason` is set ONLY when no previous briefing exists - // to stand in, so the card's honest "a figure couldn't be verified" - // state still fires exactly when the user really has no briefing. - if (ungrounded.length > 0 && insights && typeof insights === "object") { - let fallbackBriefing: unknown = null; - if (cachedText) { - try { - const prev = JSON.parse(cachedText) as Record; - fallbackBriefing = prev.dailyBriefing ?? null; - } catch { - // Unparseable previous payload — nothing safe to stand in. - } - } - (insights as Record).dailyBriefing = fallbackBriefing; - briefingOmittedReason = fallbackBriefing == null ? "ungrounded" : null; - annotate({ - action: { name: "insights.generate.briefing_grounding_stripped" }, - meta: { - ungroundedCount: ungrounded.length, - briefing_fallback: - fallbackBriefing != null ? "previous-cached" : "stripped", - }, - }); - } - } - - // Outbound safety screen over the whole prose surface, before the persist. - // - // This route is user-initiated but writes the SAME cached payload the - // background generator writes, so a violation must not be persisted here - // either. The user is waiting, so unlike the background path they get an - // explicit error rather than silence -- and because nothing was written, - // the previous cached insight is still what the app renders. - const screened = screenInsightPayloadProse(insights, locale); - if (screened) { - annotate({ - action: { name: "insights.generate.outbound_blocked" }, - meta: { locale, reason: screened }, - }); - return apiError( - "The generated insight did not pass the safety check and was discarded. Try regenerating.", - 502, - { errorCode: "insights.generate.outboundScreened" }, - ); - } - - const currentSelfContext = await getSelfContextTextForUser(userId, locale); - if (currentSelfContext !== aboutMe) { - return apiError("Profile scope changed during insight generation", 409, { - errorCode: "insights.generate.profileScopeChanged", - }); - } - if (briefingForm === "short") { - insights = applyBriefingShortForm(insights); - } - const stored = withBriefingGeneratedAt(insights, new Date()); - const committed = await prisma.user.updateMany({ - where: { - id: userId, - insightsPrivacyMode: modeAtRead, - insightsExcludeMetrics: { equals: excludeMetricsAtRead }, - locale: localeAtRead, - dateOfBirth: dateOfBirthAtRead, - gender: genderAtRead, - heightCm: heightCmAtRead, - displayName: displayNameAtRead, - }, - data: { - insightsCachedAt: new Date(), - ...insightsCacheWrite(JSON.stringify(stored)), - insightsCachedLocale: locale, - insightsSnapshotHash: hashInsightSnapshot({ - features: compactFeatures, - aboutMe: aboutMe ?? null, - comparisonBaseline: comparisonSnapshot?.baseline ?? "none", - generationLocale: locale, - ...(briefingForm === "short" ? { briefingForm } : {}), - }), - }, - }); - if (committed.count === 0) { - return apiError("Insight scope changed during generation", 409, { - errorCode: "insights.generate.scopeChanged", - }); - } - - // v1.28.30 — persist the grounding omission (marker AFTER the cache - // write so it is newer than `insightsCachedAt`). The transient - // `briefingOmittedReason` on this response only reached the caller that - // forced the regenerate; every later read showed a generic "no briefing - // yet". The dated marker lets the GET surface the same honest - // "briefing withheld" state until the next successful generation. - if (briefingOmittedReason === "ungrounded") { - void recordBriefingFailure({ - userId, - reason: "briefing-ungrounded", - locale, - }); - } - - // v1.16.8 — no blanket per-status eviction here any more (nuking ~45 - // cache rows per manual regenerate deleted every hash baseline and was - // the source of the post-regenerate card-regeneration storm). Instead, - // the manual regenerate takes the cards along through the hash-gated - // queue: every scope the user has data for is enqueued, the worker - // forces each generator past its same-day cache read, and the content- - // hash gate regenerates exactly the cards whose data changed while - // refreshing the unchanged ones for free. This is the user's escape - // hatch for a card they can SEE is stale — without it the cards had no - // refresh path until the next nightly warm. - const refillScopes = await enqueueStatusRefillForUser( - userId, - normalizeLocale(locale), - ); - annotate({ - action: { name: "insights.generate.cards_refill" }, - meta: { scopes: refillScopes }, - }); - - // v1.7.0 — the dashboard snapshot embeds the pre-generated daily - // briefing read-only; drop it so the next snapshot carries the fresh - // briefing instead of the stale one. - invalidateUserInsights(userId); - - await auditLog("insights.generate", { - userId, + const outcome = await generateBriefing({ + ctx, + chain, ipAddress: getClientIp(request), - details: { - privacyMode: dbUser?.insightsPrivacyMode, - tokensUsed: result.tokensUsed, - providerType: result.providerType, - chainProviderType: workingProviderType, - fallbackHopCount, - model: result.model, - }, - }); - - annotate({ - action: { name: "insights.generate" }, - meta: { - cached: false, - providerType: result.providerType, - chainProviderType: workingProviderType, - fallbackHopCount, - model: result.model, - tokensUsed: result.tokensUsed, - }, - }); - - return apiSuccess({ - insights: await insightsPayloadForToday(stored, { - userId, - timezone: postTimezone, - language: locale, - }), - cached: false, - legacyPayload: false, - // v1.28.28 (#470) — additive: non-null when the grounding gate stripped - // the briefing from THIS generation, so the card can explain the hole. - briefingOmittedReason, }); + return outcome.ok ? apiSuccess(outcome.data) : refuse(outcome.refusal); }), ); diff --git a/src/app/api/insights/medication-compliance-status/route.ts b/src/app/api/insights/medication-compliance-status/route.ts index 2be9b3368..1e13ff12d 100644 --- a/src/app/api/insights/medication-compliance-status/route.ts +++ b/src/app/api/insights/medication-compliance-status/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from "next/server"; import { apiSuccess } from "@/lib/api-response"; import { generateMedicationComplianceStatusForUser } from "@/lib/insights/medication-compliance-status"; -import { normalizeLocale as resolveMedicationComplianceStatusLocale } from "@/lib/insights/status-shared"; +import { normalizeLocale } from "@/lib/insights/status-shared"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; @@ -40,7 +40,7 @@ export const GET = apiHandler(async (request: NextRequest) => { userLocale: user.locale ?? null, override: localeParam, }); - const locale = resolveMedicationComplianceStatusLocale(resolved); + const locale = normalizeLocale(resolved); // v1.8.3 — read-only: serve the cache, enqueue generation out of band on // a miss. The GET never awaits the provider, so opening /insights/ diff --git a/src/app/api/insights/metric-status/route.ts b/src/app/api/insights/metric-status/route.ts index 21640e778..5f490dac5 100644 --- a/src/app/api/insights/metric-status/route.ts +++ b/src/app/api/insights/metric-status/route.ts @@ -34,7 +34,7 @@ import { aiCapabilityToServe } from "@/lib/ai/capabilities/gate"; import { unavailableStatusBody } from "@/lib/insights/status-unavailable"; import { prisma } from "@/lib/db"; import { generateMetricStatus } from "@/lib/insights/metric-status"; -import { normalizeLocale as resolveMetricStatusLocale } from "@/lib/insights/status-shared"; +import { normalizeLocale } from "@/lib/insights/status-shared"; import { METRIC_STATUS_IDS, getMetricStatusMeta, @@ -132,7 +132,7 @@ export const GET = apiHandler(async (request: NextRequest) => { userLocale: user.locale ?? null, override: localeParam, }); - const locale = resolveMetricStatusLocale(resolved); + const locale = normalizeLocale(resolved); const result = await generateMetricStatus({ metric, diff --git a/src/app/api/insights/mood-status/route.ts b/src/app/api/insights/mood-status/route.ts index 41fa7f304..565b885c6 100644 --- a/src/app/api/insights/mood-status/route.ts +++ b/src/app/api/insights/mood-status/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from "next/server"; import { apiSuccess } from "@/lib/api-response"; import { generateMoodStatusForUser } from "@/lib/insights/mood-status"; -import { normalizeLocale as resolveMoodStatusLocale } from "@/lib/insights/status-shared"; +import { normalizeLocale } from "@/lib/insights/status-shared"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; @@ -47,7 +47,7 @@ export const GET = apiHandler(async (request: NextRequest) => { userLocale: user.locale ?? null, override: localeParam, }); - const locale = resolveMoodStatusLocale(resolved); + const locale = normalizeLocale(resolved); // v1.8.3 — read-only: serve the cache, enqueue generation out of band on // a miss. The GET never awaits the provider, so opening /insights/ diff --git a/src/app/api/insights/pulse-status/route.ts b/src/app/api/insights/pulse-status/route.ts index 76b5a0f2c..4811fe1f4 100644 --- a/src/app/api/insights/pulse-status/route.ts +++ b/src/app/api/insights/pulse-status/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from "next/server"; import { apiSuccess } from "@/lib/api-response"; import { generatePulseStatusForUser } from "@/lib/insights/pulse-status"; -import { normalizeLocale as resolvePulseStatusLocale } from "@/lib/insights/status-shared"; +import { normalizeLocale } from "@/lib/insights/status-shared"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; @@ -40,7 +40,7 @@ export const GET = apiHandler(async (request: NextRequest) => { userLocale: user.locale ?? null, override: localeParam, }); - const locale = resolvePulseStatusLocale(resolved); + const locale = normalizeLocale(resolved); // v1.8.3 — read-only: serve the cache, enqueue generation out of band on // a miss. The GET never awaits the provider, so opening /insights/ diff --git a/src/app/api/insights/weight-status/route.ts b/src/app/api/insights/weight-status/route.ts index 65a185874..43e916d4c 100644 --- a/src/app/api/insights/weight-status/route.ts +++ b/src/app/api/insights/weight-status/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from "next/server"; import { apiSuccess } from "@/lib/api-response"; import { generateWeightStatusForUser } from "@/lib/insights/weight-status"; -import { normalizeLocale as resolveWeightStatusLocale } from "@/lib/insights/status-shared"; +import { normalizeLocale } from "@/lib/insights/status-shared"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; @@ -40,7 +40,7 @@ export const GET = apiHandler(async (request: NextRequest) => { userLocale: user.locale ?? null, override: localeParam, }); - const locale = resolveWeightStatusLocale(resolved); + const locale = normalizeLocale(resolved); // v1.8.3 — read-only: serve the cache, enqueue generation out of band on // a miss. The GET never awaits the provider, so opening /insights/ diff --git a/src/app/api/measurements/series/route.ts b/src/app/api/measurements/series/route.ts index 5fccd4d6c..eca0eefdf 100644 --- a/src/app/api/measurements/series/route.ts +++ b/src/app/api/measurements/series/route.ts @@ -14,10 +14,8 @@ * window, judged by that direction) and `referenceLines` (the person's own * target lines, else the clinical ones, in the unit of the points). */ -import { readDenseSeries } from "@/lib/measurements/dense-series-read"; import { NextRequest } from "next/server"; import { z } from "zod/v4"; -import { prisma } from "@/lib/db"; import { auditLog } from "@/lib/auth/audit"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { @@ -27,94 +25,9 @@ import { sanitiseZodIssues, } from "@/lib/api-response"; import { annotate } from "@/lib/logging/context"; -import { summarize, type DataPoint } from "@/lib/analytics/trends"; import { redactForExcerpt } from "@/lib/observability/redact-payload"; -import type { MeasurementType, SleepStage } from "@/generated/prisma/client"; -import { readSleepNights } from "@/lib/measurements/list/sleep-nights"; -import { VALUE_RANGES } from "@/lib/validations/measurement"; -import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; -import { usesHourlyMeanDay } from "@/lib/measurements/day-statistic"; -import { readingsMean } from "@/lib/measurements/day-mean"; -import { seriesRowsFrom } from "@/lib/measurements/series-canonical"; -import { buildSourceRankCase } from "@/lib/analytics/source-rank-sql"; -import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; -import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose"; -import { resolveTrendVerdict } from "@/lib/targets/trend-verdict"; -import { - computeSeriesChange, - seriesDayValues, - seriesReferenceValue, -} from "@/lib/measurements/series-change"; -import { resolveSeriesReferenceLines } from "@/lib/measurements/series-reference-lines"; -import { getServerTranslator } from "@/lib/i18n/server-translator"; -import { resolveJobLocale } from "@/lib/i18n/job-locale"; -import { READING_SPREAD, standardDeviation } from "@/lib/stats/spread"; - -/** - * v1.28.25 — sample-dense kinds read raw rows only up to this window. - * A CGM account stores ~288 BLOOD_GLUCOSE rows/day (105k+ rows/year) and - * per-sample PULSE piles up similarly, so the 365 / 3650-day requests - * walked a six-figure row set into JS, mapped it twice, and serialized - * everything. Mirrors the sleep precedent above, but instead of silently - * clamping the window, requests beyond the cap keep their full range and - * are day-bucketed in SQL (one point per day, mean + min/max band) — the - * chart still shows the whole requested history, just at daily grain. - * The short windows users actually browse (7 / 30 / 90 d) stay raw and - * byte-identical. Sparse kinds (weight, BP, …) are untouched. - */ -const DENSE_SERIES_RAW_WINDOW_DAYS = 90; -/** - * The kinds a device can write at sampling rate. Heart-rate variability and - * blood oxygen joined pulse and glucose once watches and rings began writing - * them every few minutes overnight: their 3650-day "Alle" range read every - * raw row, which is the read this rule exists to stop. - */ -const DENSE_SERIES_KINDS: ReadonlySet = new Set([ - "glucose", - "pulse", - "heartRateVariability", - "oxygenSaturation", -]); - -/** - * #1023 — inside the raw window, a dense-kind series (pulse, glucose, HRV, - * blood oxygen) with more rows than this is bucketed per local hour in SQL - * instead of sent raw. - * - * A watch that records heart rate once a minute puts 43 000 rows in the - * default 30-day window and 130 000 in 90 days: a 15 MB response the client - * could not draw at that resolution anyway, built from as many objects on the - * server. A CGM reading every five minutes puts 26 000 glucose rows in 90 - * days, the same problem at a smaller scale. Hourly buckets carry the hour's - * mean (pulse also its low/high band, the shape an hourly-bucket import - * already has) and cap the answer at 24 points a day; the statistics stay - * computed over every raw reading. The cap sits far above what sparse sources - * produce (a cuff, fingerstick glucose, hourly buckets over 90 days, a watch - * sampling every five minutes over 30 days), so those series stay raw and - * unchanged. - */ -const DENSE_SERIES_RAW_ROW_CAP = 10_000; - -const kindEnum = z.enum([ - "weight", - "bloodPressure", - "pulse", - "bodyFat", - "glucose", - "sleep", - "steps", - "totalBodyWater", - "boneMass", - "oxygenSaturation", - // v1.5.5 — the iOS app surfaces these as series-capable, but - // the route used to reject the trend/detail view request with a - // 422. The underlying MeasurementType enum already carries each - // value; this list extends the camelCase wire shape so the route - // matches the data it can serve. - "restingHeartRate", - "heartRateVariability", - "vo2Max", -]); +import { kindEnum } from "@/lib/measurements/series/kinds"; +import { readSeries } from "@/lib/measurements/series/read-series"; const querySchema = z.object({ kind: kindEnum, @@ -127,80 +40,6 @@ const querySchema = z.object({ days: z.coerce.number().int().min(1).max(3650).optional().default(30), }); -const KIND_TO_TYPE: Record, MeasurementType> = { - weight: "WEIGHT", - bloodPressure: "BLOOD_PRESSURE_SYS", - pulse: "PULSE", - bodyFat: "BODY_FAT", - glucose: "BLOOD_GLUCOSE", - sleep: "SLEEP_DURATION", - steps: "ACTIVITY_STEPS", - totalBodyWater: "TOTAL_BODY_WATER", - boneMass: "BONE_MASS", - oxygenSaturation: "OXYGEN_SATURATION", - restingHeartRate: "RESTING_HEART_RATE", - heartRateVariability: "HEART_RATE_VARIABILITY", - vo2Max: "VO2_MAX", -}; - -/** - * v1.11.4 — explicit per-kind unit token returned at the top level so - * the client never has to infer the value's unit. `bloodPressure` reuses - * the systolic type's unit (mmHg, same for both lines). `sleep` is - * overridden at request time to `"h"` because the route returns per-night - * TIME-ASLEEP in hours rather than the canonical per-stage minutes. - * - * v1.16.16 — `glucose` is the canonical-stored unit (mg/dL) here; it is - * overridden at request time to the user's `glucoseUnit` preference and the - * point values are converted to match, so this wire DTO reads in the SAME - * unit as the CSV + FHIR exports and the blood-glucose detail page (one - * number, one engine). - */ -const SERIES_UNIT: Record, string> = { - weight: "kg", - bloodPressure: "mmHg", - pulse: "bpm", - bodyFat: "%", - glucose: "mg/dL", - sleep: "h", - steps: "steps", - totalBodyWater: "kg", - boneMass: "kg", - oxygenSaturation: "%", - restingHeartRate: "bpm", - heartRateVariability: "ms", - vo2Max: "mL/(kg·min)", -}; - -interface SeriesPoint { - id: string; - at: string; - value: number; - secondary: number | null; - /** - * v1.11.4 — per-stage minutes for a sleep night point. `null` for - * every non-sleep kind. Lets a sleep detail view render the night's - * stage breakdown without a second fetch; the headline `value` stays - * the night's TIME-ASLEEP total. - */ - sleepStages?: Partial> | null; - /** - * v1.19.2 (iOS #34 extension) — per-point spread for `kind=pulse`. On an - * aggregated hourly heart-rate bucket `value` is the hour's AVERAGE bpm - * and these carry the hour's MIN / MAX. `null` for a per-sample PULSE row - * (no bucket spread) and absent for every non-pulse kind, so a client can - * render a low/high band only where the data supports it. - */ - valueMin?: number | null; - valueMax?: number | null; -} - -function stdDev(values: number[]): number { - if (values.length < 2) return 0; - const sd = standardDeviation(values, READING_SPREAD) ?? 0; - return Math.round(sd * 100) / 100; -} - export const GET = apiHandler(async (request: NextRequest) => { const { user, actor } = await requireRecordAuth("read", "measurements"); @@ -250,503 +89,7 @@ export const GET = apiHandler(async (request: NextRequest) => { } const { kind, days } = parsed.data; - const since = new Date(Date.now() - days * 86_400_000); - - // v1.11.4 — explicit unit token so the client never infers the unit - // from the kind. Sleep is special-cased below (per-night TIME-ASLEEP - // in HOURS); every other kind carries its canonical stored unit. - let unit = SERIES_UNIT[kind]; - - let points: SeriesPoint[] = []; - // v1.16.16 — when glucose stats are computed over RAW mg/dL (parity with the - // detail page + FHIR), the branch fills this so the response below skips the - // generic over-points path that would double-round mmol/L figures. - // v1.28.25 — the dense day-bucket branch also fills it (stats stay - // aggregated over the RAW rows in SQL, not over the day-bucket points). - let statsOverride: { - mean: number; - min: number; - max: number; - stdDev: number; - count: number; - } | null = null; - if (kind === "sleep") { - // SLEEP_DURATION is stored one row per STAGE per night (minutes). - // Collapse the stage rows into ONE point per night carrying the - // night's TIME ASLEEP (CORE + DEEP + REM, excluding IN_BED + AWAKE), - // converted to HOURS. The single-stage rows the legacy path returned - // made the chart show one fragment per stage instead of a nightly - // trend. The per-night reconstruction clusters stages into sessions - // (so a midnight-spanning night stays one point) and collapses a - // dual-source night to one canonical source via the user's `sleep` - // priority ladder. - // - // Sleep is the one kind read row-per-stage rather than from the rollup - // tier, so the nights are named in Postgres and reconstructed a chunk at - // a time (`readSleepNights`): "All" reaches as far back as the history - // does, and the stage rows in the heap stay a chunk's worth. - const [tz, priorityJson] = await Promise.all([ - resolveUserTimezone(user.id), - loadUserSourcePriority(user.id), - ]); - const nights = await readSleepNights(user.id, tz, priorityJson, { - from: since, - // A window of `days` days holds at most one night more than that. - maxNights: days + 1, - }); - unit = "h"; - points = nights - .filter((n) => n.asleepMinutes > 0) - .map((n) => { - const stageHours = Object.fromEntries( - Object.entries(n.stages).map(([s, m]) => [ - s, - Math.round((m / 60) * 100) / 100, - ]), - ) as Partial>; - return { - id: `sleep:${n.night}`, - at: n.measuredAt.toISOString(), - value: Math.round((n.asleepMinutes / 60) * 100) / 100, - secondary: null, - sleepStages: Object.keys(stageHours).length > 0 ? stageHours : null, - }; - }); - } else if (kind === "bloodPressure") { - // Exclude non-physiological rows (systolic < 40 / diastolic < 20) from the - // series AND the implicit "latest" (the last point). BP is two rows paired - // client-side, so a corrupt row that slipped in before the input floor was - // enforced — a seed-era systolic-0, iOS #33 — must never surface as the - // latest reading. Floors mirror VALUE_RANGES (the input validator's min); - // this is a read-side selection guard, not a tightening of the write floor. - // - // The ladder picks ONE source per day for the reading, not one per type: - // the source is chosen from the day's systolic rows that pass the floor, - // and both halves are read from it. Two per-type picks diverge whenever a - // source holds only half of a reading on a day (a deleted diastolic, a - // sub-floor systolic, an orphaned sample), and the pairing below then - // joined one source's systolic to another source's diastolic. Picking on - // floor-passing systolic rows also keeps a corrupt row from claiming a day - // it then contributes nothing to. The day is the UTC day, as in every - // other ladder read (`canonicalMeasurementsCte`), and the pick looks at - // the whole edge day, not only the slice after `since`. - // - // `rank` is the closed-enum whitelist splice from `buildSourceRankCase`; - // every request value is a positional parameter. - const rank = buildSourceRankCase( - await loadUserSourcePriority(user.id), - '"type"', - '"source"', - ); - const bpRows = await prisma.$queryRawUnsafe< - Array<{ - id: string; - type: "BLOOD_PRESSURE_SYS" | "BLOOD_PRESSURE_DIA"; - source: string; - value: number; - measured_at: Date; - }> - >( - ` - SELECT m."id", m."type"::text AS type, m."source"::text AS source, - m."value", m."measured_at" - FROM measurements m - JOIN ( - SELECT DISTINCT ON (date_trunc('day', "measured_at")) - date_trunc('day', "measured_at") AS d, - "source" AS canon - FROM measurements - WHERE "user_id" = $1 - AND "type" = 'BLOOD_PRESSURE_SYS'::"measurement_type" - AND "deleted_at" IS NULL - AND "value" >= $3 - AND "measured_at" >= date_trunc('day', $2::timestamptz AT TIME ZONE 'UTC') - ORDER BY date_trunc('day', "measured_at"), (${rank}), "source"::text - ) c - ON c.d = date_trunc('day', m."measured_at") - AND c.canon = m."source" - WHERE m."user_id" = $1 - AND m."deleted_at" IS NULL - AND m."measured_at" >= $2 - AND ( - (m."type" = 'BLOOD_PRESSURE_SYS'::"measurement_type" AND m."value" >= $3) - OR (m."type" = 'BLOOD_PRESSURE_DIA'::"measurement_type" AND m."value" >= $4) - ) - ORDER BY m."measured_at" ASC, m."id" ASC - `, - user.id, - since, - VALUE_RANGES.BLOOD_PRESSURE_SYS.min, - VALUE_RANGES.BLOOD_PRESSURE_DIA.min, - ); - const sys = bpRows.filter((r) => r.type === "BLOOD_PRESSURE_SYS"); - // Diastolic rows per source: a pair is only ever made inside one source, - // which also holds across midnight, where the two days may have picked - // different sources. - const diaBySource = new Map(); - for (const r of bpRows) { - if (r.type !== "BLOOD_PRESSURE_DIA") continue; - const list = diaBySource.get(r.source); - if (list) list.push(r); - else diaBySource.set(r.source, [r]); - } - - // Pair by closest timestamp within ±5 minutes. Both reads arrive - // sorted ascending, so the nearest-diastolic index is non-decreasing - // across the systolic walk — a two-pointer merge (one pointer per - // source) finds each pair in O(sys + dia) instead of the previous full - // diastolic rescan per systolic row (O(sys × dia) — quadratic on dense - // imports). Ties keep the earlier diastolic row, matching the old - // scan's strict-`<` keep. - const PAIR_WINDOW_MS = 5 * 60_000; - const diaIdx = new Map(); - points = sys.map((s) => { - const sysMs = s.measured_at.getTime(); - const dia = diaBySource.get(s.source) ?? []; - let idx = diaIdx.get(s.source) ?? 0; - while ( - idx + 1 < dia.length && - Math.abs(dia[idx + 1].measured_at.getTime() - sysMs) < - Math.abs(dia[idx].measured_at.getTime() - sysMs) - ) { - idx += 1; - } - diaIdx.set(s.source, idx); - const best = dia[idx]; - const paired = - best !== undefined && - Math.abs(best.measured_at.getTime() - sysMs) <= PAIR_WINDOW_MS; - return { - id: s.id, - at: s.measured_at.toISOString(), - value: s.value, - secondary: paired ? best.value : null, - }; - }); - } else if ( - (DENSE_SERIES_KINDS.has(kind) && days > DENSE_SERIES_RAW_WINDOW_DAYS) || - (DENSE_SERIES_KINDS.has(kind) && - (await prisma.measurement.count({ - where: { - userId: user.id, - type: KIND_TO_TYPE[kind], - measuredAt: { gte: since }, - deletedAt: null, - }, - })) > DENSE_SERIES_RAW_ROW_CAP) - ) { - // Day buckets past the raw window; hour buckets for a dense-kind - // stream too dense to send raw inside it. - const grain = days > DENSE_SERIES_RAW_WINDOW_DAYS ? "day" : "hour"; - // v1.28.25 — long-window read of a sample-dense kind (CGM glucose, - // per-sample / hourly pulse). Day-bucket in SQL instead of walking - // every raw row into JS: one aggregate pass in Postgres returns at - // most `days` rows regardless of sample density. Bound parameters - // mirror the rollup tier's `date_trunc(... ) GROUP BY` aggregate - // (measurement-rollups.ts), and the rows are the ladder-winning source per - // day, the same rule the rollup tier and sleep apply. - const [userTz, priorityJson] = await Promise.all([ - resolveUserTimezone(user.id), - loadUserSourcePriority(user.id), - ]); - const type = KIND_TO_TYPE[kind]; - const seriesRows = seriesRowsFrom(priorityJson, type, days); - // A day of a type with an activity-dependent sampling rate (pulse) is the - // weighted mean of its local hours' means, so a workout hour with hundreds - // of times the readings does not outweigh the rest of the day (see - // `day-statistic.ts`). The hour grain is already one mean per hour. - const { bucketRows, rawAgg } = await readDenseSeries({ - userId: user.id, - type, - grain, - userTz, - since, - seriesRows, - }); - const round2 = (v: number) => Math.round(v * 100) / 100; - const dayId = (d: Date) => - grain === "day" - ? `day:${userDayKey(d, userTz)}` - : `hour:${d.toISOString()}`; - if (kind === "glucose") { - // Same unit engine as the raw glucose branch: canonical mg/dL in - // the DB, converted ONCE at serialization to the user's preference. - const profile = await prisma.user.findUnique({ - where: { id: user.id }, - select: { glucoseUnit: true }, - }); - const glucoseUnit = resolveGlucoseUnit(profile?.glucoseUnit ?? null); - unit = glucoseUnit; - points = bucketRows.map((r) => ({ - id: dayId(r.bucket_start), - at: r.bucket_start.toISOString(), - value: convertGlucose(r.mean, glucoseUnit), - secondary: null, - })); - if ( - rawAgg !== undefined && - rawAgg.n > 0 && - rawAgg.mean !== null && - rawAgg.min !== null && - rawAgg.max !== null - ) { - // Mirrors the raw branch's v1.16.16 parity: round the mg/dL - // aggregate to 2 decimals (summarize()'s convention), then - // convert each figure once. - statsOverride = { - mean: convertGlucose(round2(rawAgg.mean), glucoseUnit), - min: convertGlucose(rawAgg.min, glucoseUnit), - max: convertGlucose(rawAgg.max, glucoseUnit), - stdDev: convertGlucose(round2(rawAgg.sd ?? 0), glucoseUnit), - count: rawAgg.n, - }; - } - } else { - // `value` is the bucket's average. For pulse, valueMin/valueMax carry - // the bucket's low/high band (folding each hourly import bucket's own - // spread via the COALESCE above), same band semantics as the per-hour - // shape. The other dense kinds keep the bare point shape their raw rows - // have, so a client reads a bucket the way it reads a reading. - points = bucketRows.map((r) => ({ - id: dayId(r.bucket_start), - at: r.bucket_start.toISOString(), - value: round2(r.mean), - secondary: null, - ...(kind === "pulse" - ? { valueMin: r.min_value, valueMax: r.max_value } - : {}), - })); - if ( - rawAgg !== undefined && - rawAgg.n > 0 && - rawAgg.mean !== null && - rawAgg.min !== null && - rawAgg.max !== null - ) { - statsOverride = { - mean: round2(rawAgg.mean), - min: rawAgg.min, - max: rawAgg.max, - stdDev: round2(rawAgg.sd ?? 0), - count: rawAgg.n, - }; - } - } - } else { - const type = KIND_TO_TYPE[kind]; - // v1.19.2 (iOS #34 extension) — pull the per-bucket spread for the - // heart-rate kind so the chart can render a low/high band around the - // hourly average. Only PULSE rows ever carry a non-null spread (the - // hourly HR bucket); every other kind selects the bare value shape. - const includeSpread = kind === "pulse"; - // The ladder-winning source per day (see `seriesRowsFrom`), so a second - // provider neither blends into nor duplicates the series. Bounded as before: - // every dense kind reaches this read only inside 90 days and under 10 000 - // rows (counted first), and the sparse kinds are a few readings a day. - const sourcePriority = await loadUserSourcePriority(user.id); - const rawRows = await prisma.$queryRawUnsafe< - Array<{ - id: string; - value: number; - measured_at: Date; - value_min: number | null; - value_max: number | null; - }> - >( - ` - SELECT m."id", m."value", m."measured_at", m."value_min", m."value_max" - FROM ${seriesRowsFrom(sourcePriority, type, days)} - WHERE m."measured_at" >= $2 - ORDER BY m."measured_at" ASC, m."id" ASC - `, - user.id, - since, - ); - const rows = rawRows.map((r) => ({ - id: r.id, - value: r.value, - measuredAt: r.measured_at, - ...(includeSpread - ? { valueMin: r.value_min, valueMax: r.value_max } - : {}), - })); - if (kind === "glucose") { - // v1.16.16 — glucose is stored canonical mg/dL; convert each point to - // the user's display unit AT SERIALIZATION so the wire DTO is unit- - // coherent with the CSV + FHIR exports (one number, one engine). The - // top-level `unit` token follows. mg/dL-preference users are unchanged - // (convertGlucose rounds the integer); mmol/L users see 1-decimal - // values (100 → 5.5) and `unit: "mmol/L"`. - const profile = await prisma.user.findUnique({ - where: { id: user.id }, - select: { glucoseUnit: true }, - }); - const glucoseUnit = resolveGlucoseUnit(profile?.glucoseUnit ?? null); - unit = glucoseUnit; - points = rows.map((r) => ({ - id: r.id, - at: r.measuredAt.toISOString(), - value: convertGlucose(r.value, glucoseUnit), - secondary: null, - })); - // v1.16.16 — stat parity. The stat strip's mean/min/max/stdDev must - // match the detail-page + FHIR convention: aggregate over RAW mg/dL, - // then convert each resulting figure ONCE. Deriving the stats from the - // already-converted+rounded points double-rounds the mean and stdDev - // for mmol/L users, drifting the last decimal off the detail page. - // Single-reading sets stay identical (100 → 5.5). Hand back stats here - // so the shared block below leaves a populated `statsOverride` alone. - const rawDataPoints: DataPoint[] = rows.map((r) => ({ - date: r.measuredAt, - value: r.value, - })); - if (rawDataPoints.length > 0) { - const rawSummary = summarize(rawDataPoints); - const rawValues = rawDataPoints.map((p) => p.value); - statsOverride = { - mean: - rawSummary.mean === null - ? 0 - : convertGlucose(rawSummary.mean, glucoseUnit), - min: - rawSummary.min === null - ? 0 - : convertGlucose(rawSummary.min, glucoseUnit), - max: - rawSummary.max === null - ? 0 - : convertGlucose(rawSummary.max, glucoseUnit), - // Glucose mg/dL↔mmol/L is purely multiplicative (no offset), so a - // spread converts with the same factor as a level. - stdDev: convertGlucose(stdDev(rawValues), glucoseUnit), - count: rawSummary.count, - }; - } - } else if (includeSpread) { - // v1.19.2 (iOS #34 extension) — pulse points carry the per-bucket - // spread. A per-sample PULSE row has null min/max; an hourly HR bucket - // carries the hour's range around the average. - points = rows.map((r) => { - const row = r as typeof r & { - valueMin: number | null; - valueMax: number | null; - }; - return { - id: row.id, - at: row.measuredAt.toISOString(), - value: row.value, - secondary: null, - valueMin: row.valueMin ?? null, - valueMax: row.valueMax ?? null, - }; - }); - // An hourly-mean type's strip mean is the mean of its local days' - // values, each day the mean of its local hours' means, so a dense - // workout hour does not outweigh the rest of the window. Count, min, - // max and the spread stay over every reading. - if (usesHourlyMeanDay(type) && rows.length > 0) { - const userTz = await resolveUserTimezone(user.id); - const values = rows.map((r) => r.value); - const rawSummary = summarize( - rows.map((r) => ({ date: r.measuredAt, value: r.value })), - ); - const mean = readingsMean(type, rows, userTz); - statsOverride = { - mean: mean === null ? 0 : Math.round(mean * 100) / 100, - min: rawSummary.min ?? 0, - max: rawSummary.max ?? 0, - stdDev: stdDev(values), - count: rawSummary.count, - }; - } - } else { - points = rows.map((r) => ({ - id: r.id, - at: r.measuredAt.toISOString(), - value: r.value, - secondary: null, - })); - } - } - - const dataPoints: DataPoint[] = points.map((p) => ({ - date: new Date(p.at), - value: p.value, - })); - const summary = dataPoints.length > 0 ? summarize(dataPoints) : null; - const values = dataPoints.map((p) => p.value); - - // v1.43 — the resolved judgements. The profile and targets are the - // RECORD's (the chart is theirs); the labels are in the language of the - // account reading it. - const [record, reader, seriesTz] = await Promise.all([ - prisma.user.findUnique({ - where: { id: user.id }, - select: { - dateOfBirth: true, - gender: true, - heightCm: true, - thresholdsJson: true, - glucoseUnit: true, - locale: true, - }, - }), - actor.id === user.id - ? null - : prisma.user.findUnique({ - where: { id: actor.id }, - select: { locale: true }, - }), - resolveUserTimezone(user.id), - ]); - const rangeProfile = { - dateOfBirth: record?.dateOfBirth ?? null, - gender: record?.gender ?? null, - heightCm: record?.heightCm ?? null, - }; - const verdictType = KIND_TO_TYPE[kind]; - const dayValues = seriesDayValues(verdictType, points, seriesTz); - const reference = seriesReferenceValue(dayValues); - const direction = resolveTrendVerdict(verdictType, { - profile: rangeProfile, - thresholdsJson: record?.thresholdsJson ?? null, - // The verdict reads stored units: sleep is served in hours here and - // stored in minutes. Glucose has no band of its own on this chart. - referenceValue: - reference !== null && kind === "sleep" ? reference * 60 : reference, - }); - const translator = getServerTranslator( - await resolveJobLocale((reader ?? record)?.locale ?? null), + return apiSuccess( + await readSeries({ userId: user.id, actorId: actor.id, kind, days }), ); - const referenceLines = resolveSeriesReferenceLines(kind, { - profile: rangeProfile, - thresholdsJson: record?.thresholdsJson ?? null, - glucoseUnit: resolveGlucoseUnit(record?.glucoseUnit ?? null), - t: (key) => translator.t(key), - }); - - annotate({ - action: { name: "measurements.series" }, - meta: { kind, days, count: points.length }, - }); - - return apiSuccess({ - kind, - unit, - points, - direction, - change: computeSeriesChange(dayValues, direction), - referenceLines, - stats: - statsOverride ?? - (summary - ? { - mean: summary.mean, - min: summary.min, - max: summary.max, - stdDev: stdDev(values), - count: summary.count, - } - : { mean: 0, min: 0, max: 0, stdDev: 0, count: 0 }), - }); }); diff --git a/src/app/api/medications/[id]/glp1/route.ts b/src/app/api/medications/[id]/glp1/route.ts index 2f0b00f1e..a50bb44ad 100644 --- a/src/app/api/medications/[id]/glp1/route.ts +++ b/src/app/api/medications/[id]/glp1/route.ts @@ -64,7 +64,7 @@ export const GET = apiHandler( const { id } = await params; // v1.5.5 — close the §10 invariant 24 gap. The destructive - // sweep in af224964 lifted every sibling `[id]/**` handler onto the + // sweep in af224964 lifted every sibling `[id]` handler onto the // shared ownership helper; the glp1 GET still hand-rolled the // `findUnique + userId compare` pattern. Routing it through the // same predicate keeps the 404 leak shape identical across every diff --git a/src/app/api/medications/[id]/intake/import/route.ts b/src/app/api/medications/[id]/intake/import/route.ts index 934f49d45..4952a4932 100644 --- a/src/app/api/medications/[id]/intake/import/route.ts +++ b/src/app/api/medications/[id]/intake/import/route.ts @@ -105,7 +105,7 @@ export const POST = apiHandler( const { id } = await params; // v1.5.5 — route ownership through the shared helper so the - // 404 leak shape stays identical across every `[id]/**` handler. + // 404 leak shape stays identical across every `[id]` handler. const guard = await assertMedicationOwnership(id, user.id); if (guard) return guard; diff --git a/src/app/api/medications/[id]/intake/route.ts b/src/app/api/medications/[id]/intake/route.ts index b3b7fa966..7d8eebf6d 100644 --- a/src/app/api/medications/[id]/intake/route.ts +++ b/src/app/api/medications/[id]/intake/route.ts @@ -1,4 +1,3 @@ -import { prisma } from "@/lib/db"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { auditLog } from "@/lib/auth/audit"; @@ -18,32 +17,13 @@ import { unstableExternalIdMeta, unstableExternalIdShape, } from "@/lib/validations/external-id"; -import { resolveInjectionSiteForWrite } from "@/lib/medications/injection-site-write"; -import type { InjectionSiteKey } from "@/lib/medications/injection-sites"; import { withIdempotency } from "@/lib/idempotency"; -import { - consumeForIntake, - restoreForIntake, -} from "@/lib/medications/inventory/consumption"; -import { reconcileOneShotState } from "@/lib/medications/lifecycle"; import { assertMedicationOwnership, assertMedicationTakesIntake, } from "@/lib/medications/route-guards"; -import { invalidateUserMedications } from "@/lib/cache/invalidate"; -import { queueMedicationIntakeSync } from "@/lib/notifications/medication-intake-sync"; -import { dispatchMedicationIntakeWebClear } from "@/lib/notifications/web-push-clear"; -import { countOutstandingDosesToday } from "@/lib/medications/outstanding-doses"; -import { notifyDelegatedIntake } from "@/lib/notifications/delegated-intake"; -import { refreshDoseSlot } from "@/lib/medications/dose-day-refresh"; -import { - applyCanonicalSlotWrite, - findPinConflict, - mayConvergeOntoSuppliedSlot, - resolveForcedSlotForWrite, - resolveSlotForWriteByBand, - resolveSlotInstantForWrite, -} from "@/lib/medications/scheduling/slot-upsert"; +import { recordIntake } from "@/lib/medications/record-intake/record-intake"; +import { listIntakeEvents } from "@/lib/medications/list-intake-events"; import { NextRequest } from "next/server"; type RouteParams = { params: Promise<{ id: string }> }; @@ -53,25 +33,25 @@ export const POST = apiHandler( ); async function postIntake(request: NextRequest, { params }: RouteParams) { - // v1.36.x — a delegated write, the per-medication form of the same verb. + // A delegated write, the per-medication form of the same verb. // `user` is the record the dose belongs to; `actor` is whoever pressed the - // button, and is used for nothing but the owner's notification below. + // button, and is used for nothing but the owner's notification. const { user, actor, authMethod } = await requireRecordAuth( "write", "medications", ); - // v1.36.1 — the sibling route `POST /api/medications/intake` - // refuses a delegate changing a dose the owner already recorded. This route - // reaches the same rows through the slot upsert, so without the same rule it - // is simply the way round the other one: an explicit skip posted onto a - // taken slot flips the outcome and refunds the inventory the take consumed. - // The flag rides into `applyCanonicalSlotWrite`, which is where the existing - // row is already loaded, so the check adds no query and cannot race. + // The sibling route `POST /api/medications/intake` refuses a delegate + // changing a dose the owner already recorded. This route reaches the same + // rows through the slot upsert, so without the same rule it is simply the + // way round the other one: an explicit skip posted onto a taken slot flips + // the outcome and refunds the inventory the take consumed. The flag rides + // into `applyCanonicalSlotWrite`, which is where the existing row is + // already loaded, so the check adds no query and cannot race. const isDelegated = actor.id !== user.id; /** - * The refusal itself, shared by both upsert call sites so the two cannot + * The refusal itself, answered for both upsert call sites so the two cannot * drift apart. Files under the OWNER — `auditLog` stamps the actor from the * request's acting context — so a refused attempt reaches the activity panel * the same way the sibling route's does, under the same action name. @@ -92,7 +72,7 @@ async function postIntake(request: NextRequest, { params }: RouteParams) { { errorCode: "sharing.not_permitted" }, ); } - // v1.32.8 (iOS #64) — write provenance derived from the transport, never + // Write provenance derived from the transport (iOS #64), never // client-asserted: a Bearer/native call stores `API`, a cookie/browser call // stores `WEB`. The other `IntakeSource` values are producer-owned by OTHER // routes (the Telegram worker mints `REMINDER`, the bulk/import paths mint @@ -103,8 +83,8 @@ async function postIntake(request: NextRequest, { params }: RouteParams) { const intakeSource = authMethod === "bearer" ? "API" : "WEB"; const { id } = await params; - // v1.4.25 — privacy gate hoisted to the shared helper. This is a - // dose write, so the guard also refuses a record-only medication with + // Privacy gate through the shared helper. This is a dose write, so the + // guard also refuses a record-only medication with // `medication.intake.notTracked` (iOS #116, item 15): its history stays, // but a new dose would be stored and then counted nowhere. const guard = await assertMedicationTakesIntake(id, user.id); @@ -120,8 +100,8 @@ async function postIntake(request: NextRequest, { params }: RouteParams) { medicationId: id, }); if (!parsed.success) { - // v1.4.43 — per-med intake POST hot path; multi-issue 422 + - // audit breadcrumb keyed `medications.intake.create.validation-failed`. + // Per-med intake POST hot path; multi-issue 422 + audit breadcrumb keyed + // `medications.intake.create.validation-failed`. const issues = sanitiseZodIssues(parsed.error.issues); const shape = unstableExternalIdShape(body, "idempotencyKey"); annotate({ @@ -134,12 +114,12 @@ async function postIntake(request: NextRequest, { params }: RouteParams) { : {}), }, }); - // v1.4.49 — strip `message` from the audit-ledger row; the - // intake payload carries `idempotencyKey` (opaque caller string). + // Strip `message` from the audit-ledger row; the intake payload carries + // `idempotencyKey` (opaque caller string). const auditIssues = sanitiseZodIssues(parsed.error.issues, { stripValuesFromMessage: true, }); - // v1.36.x — through `auditLog()`, the only writer that stamps the actor. + // Through `auditLog()`, the only writer that stamps the actor. void auditLog("medications.intake.create.validation-failed", { userId: user.id, details: { issues: auditIssues, medicationId: id }, @@ -149,515 +129,28 @@ async function postIntake(request: NextRequest, { params }: RouteParams) { return returnAllZodIssues(parsed.error, 422); } - const { - scheduledFor, - takenAt, - skipped, - idempotencyKey, - injectionSite, - forceSlotInstant, - doseTaken, - } = parsed.data; - - // v1.16.4 — the dose override documents a consumed dose, so it only - // applies to a taken (non-skipped) write; on a skip it is silently - // dropped (the slot consumed nothing). - const resolvedDoseTaken = !skipped && doseTaken ? doseTaken : null; - - // v1.8.5 — resolve + server-validate the optional injection site. Load - // the medication's delivery form + tracking opt-in + per-medication - // allowed sites, plus the user's global exclusion deny-list. A site - // outside the effective allowed set is a hard 422; a site on a - // non-injection / tracking-off med (or a skip) is silently dropped. - let resolvedInjectionSite: InjectionSiteKey | null = null; - if (injectionSite !== undefined) { - const [med, userRow] = await Promise.all([ - prisma.medication.findUnique({ - where: { id }, - select: { - deliveryForm: true, - trackInjectionSites: true, - allowedInjectionSites: true, - }, - }), - prisma.user.findUnique({ - where: { id: user.id }, - select: { globalExcludedInjectionSites: true }, - }), - ]); - const resolution = resolveInjectionSiteForWrite({ - submitted: injectionSite, - taken: !skipped, - deliveryForm: med?.deliveryForm ?? "ORAL", - trackInjectionSites: med?.trackInjectionSites ?? false, - allowedInjectionSites: (med?.allowedInjectionSites ?? - []) as InjectionSiteKey[], - globalExcludedInjectionSites: (userRow?.globalExcludedInjectionSites ?? - []) as InjectionSiteKey[], - }); - if (resolution.kind === "disallowed") { - annotate({ - action: { name: "medication.intake.injection_site.disallowed" }, - meta: { medication_id: id, site: resolution.site }, - }); - return apiError( - "Injection site is not allowed for this medication", - 422, - { - errorCode: "medications.intake.injection_site.disallowed", - }, - ); - } - resolvedInjectionSite = resolution.site; - } - - const resolvedTakenAt = skipped ? null : (takenAt ?? new Date()); - const incomingScheduledFor = scheduledFor ?? takenAt ?? new Date(); - // The per-med route only ever carries an explicit user gesture: - // `resolvedTakenAt` is `now()` for any non-skip POST (there is no - // "mark pending" write on this route), so a non-skip is an explicit - // taken and a `skipped:true` body is an explicit skip. Neither is a - // pending projection echo, so the no-downgrade guard never trips here — - // it is the bulk/sync route that replays pending echoes. The flags are - // threaded through so the shared upsert applies last-write-wins. - const isExplicitTaken = !skipped; - const isExplicitSkip = skipped === true; - - // v1.15.18 — window-band slot attribution (replaces the wide ±6h - // `snapToleranceMs` nearest-snap). The take is bound to a slot by - // membership in that slot's configurable dose window — the SAME bands the - // read ledger + the compliance % consume, so the three surfaces can never - // disagree. A take that lands in no window is ad-hoc (`canonicalSlot` null) - // and records as a standalone "taken now" row; PRN meds always insert - // standalone. Resolved BEFORE the idempotency/dedup window so a scheduled - // dose routes through the slot upsert (which is itself the dedup). - // - // - SKIP: a skip is logged against a slot deliberately (it carries no - // `takenAt` to attribute by), so it keeps the canonical `scheduledFor` - // snap that binds it to the slot's pending REMINDER row. - // - TAKEN: attribute by `takenAt` band membership. The optional - // `forceSlotInstant` pins an off-window take onto a chosen real slot - // ("diesem Slot zuordnen?"); a pin that is not a real slot is a 422. - let canonicalSlot: Date | null = null; - // v1.15.20 — binding provenance for the written row: USER_PIN on the - // forced "diesem Slot zuordnen" path, AUTO when band attribution decided. - // Skips carry no binding decision (undefined → column untouched/default). - let attributionSource: "AUTO" | "USER_PIN" | undefined; - if (skipped) { - if (scheduledFor === undefined) { - // v1.16.9 — a slot-less deliberate skip resolves by band membership - // on the skip moment, like a take does. The defaulted-now snap only - // reached the tight reminder-grace window, so a skip tapped past it - // landed as an orphan ad-hoc row while the slot's pending REMINDER - // row stayed open to auto-miss. - const attribution = await resolveSlotForWriteByBand({ - userId: user.id, - medicationId: id, - userTz: user.timezone, - takenAt: incomingScheduledFor, - }); - canonicalSlot = attribution.slotInstant; - } else { - canonicalSlot = await resolveSlotInstantForWrite({ - userId: user.id, - medicationId: id, - userTz: user.timezone, - incoming: incomingScheduledFor, - instantIsExplicit: true, - isTakenWrite: false, - }); - } - } else if (forceSlotInstant !== undefined) { - canonicalSlot = await resolveForcedSlotForWrite({ - userId: user.id, + const outcome = await recordIntake( + { + user, + actorId: actor.id, medicationId: id, - userTz: user.timezone, - slotInstant: forceSlotInstant, - }); - if (canonicalSlot === null) { - annotate({ - action: { name: "medication.intake.force_slot.invalid" }, - meta: { medication_id: id }, - }); - return apiError( - "forceSlotInstant is not a scheduled slot of this medication", - 422, - { errorCode: "medications.intake.force_slot.invalid" }, - ); - } - // v1.16.0 — refuse to pin onto a slot another recorded action already - // serves: the explicit-write last-write-wins rule would silently - // overwrite that dose record. The ledger UI only offers the pin for - // unserved slots, so this only fires for stale clients / raw API calls. - if ( - await findPinConflict({ - userId: user.id, - medicationId: id, - canonicalSlot, - incomingTakenAt: resolvedTakenAt ?? null, - }) - ) { - annotate({ - action: { name: "medication.intake.force_slot.occupied" }, - meta: { medication_id: id }, - }); - return apiError( - "forceSlotInstant already carries a recorded dose action", - 422, - { errorCode: "medications.intake.force_slot.occupied" }, - ); - } - attributionSource = "USER_PIN"; - } else { - // A non-skip write on this route always carries a `takenAt` (defaulted to - // now), so `resolvedTakenAt` is non-null here; the fallback only guards the - // type. - const attribution = await resolveSlotForWriteByBand({ - userId: user.id, - medicationId: id, - userTz: user.timezone, - takenAt: resolvedTakenAt ?? incomingScheduledFor, - }); - canonicalSlot = attribution.slotInstant; - // A band decision (slot or ad-hoc) is an AUTO binding; it also resets a - // stale USER_PIN when this write converges onto a previously-pinned row. - attributionSource = "AUTO"; - - // The client can NAME the dose it is recording, and when it does, that - // answer outranks the band. The Verlauf ledger's "Genommen" posts the - // displayed slot's own anchor, and the medication card posts its - // display-due slot; both mean "record THIS dose". Band attribution answers - // a different question — which slot the take's clock time falls into — so - // backfilling a slot from an earlier day matched no band, fell through to - // the standalone insert, and left the named slot unserved: the entry - // reverted to missed on the next read and the dose the user recorded was - // filed on the wrong day. - // - // Only a REAL slot of this medication is honoured (the same band-anchor - // validation the "diesem Slot zuordnen" pin runs), so an arbitrary client - // instant still records ad-hoc rather than parking a live row on a - // fabricated anchor. And only a slot that does not sit ahead of the take, - // by the same forward guard the convergence probe below uses — a - // late-morning dose must never consume the evening slot. - if ( - scheduledFor !== undefined && - mayConvergeOntoSuppliedSlot({ - skipped, - takenAt: resolvedTakenAt, - suppliedSlot: incomingScheduledFor, - }) - ) { - const namedSlot = await resolveForcedSlotForWrite({ - userId: user.id, - medicationId: id, - userTz: user.timezone, - slotInstant: incomingScheduledFor, - }); - // Only when the naming actually decides something the clock did not. - // A dose taken inside its own window resolves to the same anchor either - // way and stays an AUTO binding, so an ordinary on-time take is - // unchanged. When the two disagree the binding rests on the user's - // choice, which is what USER_PIN records — and the read ledger binds a - // USER_PIN take by its stored anchor rather than by the clock, so a - // backfilled dose reads under the slot it was recorded for instead of - // falling back to ad-hoc. - if ( - namedSlot !== null && - namedSlot.getTime() !== canonicalSlot?.getTime() - ) { - canonicalSlot = namedSlot; - attributionSource = "USER_PIN"; - } - } - } - - // Idempotency check (explicit key or server-side dedup window) - if (idempotencyKey) { - const existing = await prisma.medicationIntakeEvent.findFirst({ - where: { - idempotencyKey, - userId: user.id, - medicationId: id, - }, - }); - if (existing) { - return apiSuccess(existing); - } - } else if (!canonicalSlot) { - // Unscheduled / PRN only — the slot upsert handles dedup for - // scheduled doses by collapsing onto the canonical slot row. - // Server-side dedup: prevent double-logging within 60 seconds. - const recentDuplicate = await prisma.medicationIntakeEvent.findFirst({ - where: { - userId: user.id, - medicationId: id, - skipped, - createdAt: { gte: new Date(Date.now() - 60_000) }, - }, - orderBy: { createdAt: "desc" }, - }); - if (recentDuplicate) { - return apiSuccess(recentDuplicate); - } - } - - let event; - // v1.8.2 — whether this write moved the slot pending→taken. - // Only that transition decrements pen inventory (M2). For the - // unscheduled/PRN branch a non-skip write always records a fresh dose, - // so it consumes when not skipped. - let consumedTransition = !skipped; - if (canonicalSlot) { - // Scheduled dose — converge onto the one canonical slot row regardless - // of `source` (the pending REMINDER row, or any prior row for this - // slot) through the shared upsert: H1 deterministic selection, C2 - // no-downgrade guard, and a C1 race-safe create that re-finds + updates - // on a P2002 collision rather than 500-ing or duplicating. - const applied = await applyCanonicalSlotWrite({ - client: prisma, - userId: user.id, - medicationId: id, - canonicalSlot, - takenAt: resolvedTakenAt, - skipped, - isExplicitTaken, - isExplicitSkip, - idempotencyKey: idempotencyKey ?? null, - createSource: intakeSource, - refuseOutcomeChange: isDelegated, - // v1.8.5 — resolved + validated site (null unless a tracking-on - // injection taken write supplied an allowed site). - injectionSite: resolvedInjectionSite, - attributionSource, - // v1.16.4 — per-intake dose override (null unless a taken write - // carried one). - doseTaken: resolvedDoseTaken, - }); - if (applied.outcomeChangeRefused) return refuseOutcomeChange(); - event = applied.row; - consumedTransition = applied.consumedTransition; - // Reset the snooze when a dose is actually recorded (not on a - // no-downgrade no-op, which left the prior taken row untouched). - if (!skipped && !applied.noDowngradeNoOp) { - await prisma.medication.update({ - where: { id }, - data: { snoozedUntil: null }, - }); - } - } else { - // Unscheduled / PRN / off-slot. When the client named an explicit - // `scheduledFor`, converge source-agnostically onto any live row that - // already sits on that instant (e.g. the pending REMINDER row the - // worker minted on a slot the band attribution did not claim) before - // inserting. Without the probe the insert lands a second live row for - // the same slot that differs only by `source` — the partial unique - // index carries `source` and cannot catch it — inflating the - // compliance rollup's scheduled count. A defaulted anchor (takenAt / - // now) never names a slot, so the probe is skipped on that hot path. - // - // Dose-safety guard: a TAKEN write must not converge onto a slot whose - // anchor is in the future relative to the take. The card advances its - // display-due to the evening slot once the morning slot's catch-up - // window lapses, so a late-morning "Genommen" posts the evening slot as - // `scheduledFor`; band attribution already rejected the take, and the - // probe must not re-bind it forward onto the evening pending row (a - // late-morning dose silently consuming the 21:00 slot). It records - // standalone (ad-hoc) instead. - const existingSlotRow = - scheduledFor !== undefined && - mayConvergeOntoSuppliedSlot({ - skipped, - takenAt: resolvedTakenAt, - suppliedSlot: incomingScheduledFor, - }) - ? await prisma.medicationIntakeEvent.findFirst({ - where: { - userId: user.id, - medicationId: id, - scheduledFor: incomingScheduledFor, - deletedAt: null, - }, - select: { id: true }, - }) - : null; - if (existingSlotRow) { - const applied = await applyCanonicalSlotWrite({ - client: prisma, - userId: user.id, - medicationId: id, - canonicalSlot: incomingScheduledFor, - takenAt: resolvedTakenAt, - skipped, - isExplicitTaken, - isExplicitSkip, - idempotencyKey: idempotencyKey ?? null, - createSource: intakeSource, - refuseOutcomeChange: isDelegated, - injectionSite: resolvedInjectionSite, - attributionSource, - doseTaken: resolvedDoseTaken, - }); - if (applied.outcomeChangeRefused) return refuseOutcomeChange(); - event = applied.row; - consumedTransition = applied.consumedTransition; - if (!skipped && !applied.noDowngradeNoOp) { - await prisma.medication.update({ - where: { id }, - data: { snoozedUntil: null }, - }); - } - } else { - // Genuinely standalone. Anchor a taken write on the intake instant — - // the documented ad-hoc contract (`scheduledFor = takenAt`) — so an - // unresolvable client anchor can never park a live row exactly on a - // slot instant a pending REMINDER row is minted for later. A skip - // without a slot keeps the incoming instant (it has no takenAt). - [event] = await prisma.$transaction([ - prisma.medicationIntakeEvent.create({ - data: { - userId: user.id, - medicationId: id, - scheduledFor: resolvedTakenAt ?? incomingScheduledFor, - takenAt: resolvedTakenAt, - skipped, - source: intakeSource, - idempotencyKey: idempotencyKey ?? null, - // v1.8.5 — site only on a resolved taken-injection write. - ...(resolvedInjectionSite !== null && { - injectionSite: resolvedInjectionSite, - }), - // v1.16.4 — dose override only on a taken write carrying one. - ...(resolvedDoseTaken !== null && { - doseTaken: resolvedDoseTaken, - }), - }, - }), - // Reset snooze when medication is taken - ...(!skipped - ? [ - prisma.medication.update({ - where: { id }, - data: { snoozedUntil: null }, - }), - ] - : []), - ]); - } - } - - // v1.16.10 — inventory consumption / restore. A taken write consumes - // `unitsPerDose` units (the stamp on the event row makes replays and - // re-posts exactly-once; `consumedTransition` skips the read for the - // no-op echo cases the slot upsert already classified). An explicit - // skip can downgrade a previously-taken row (last-write-wins), so it - // refunds whatever that row's stamp recorded. Both hooks are - // best-effort and never block the intake write. - if (!skipped && consumedTransition) { - await consumeForIntake({ - client: prisma, - userId: user.id, - medicationId: id, - eventId: event.id, - intakeAt: event.takenAt ?? event.scheduledFor, - }); - } else if (skipped) { - await restoreForIntake({ - client: prisma, - userId: user.id, - eventId: event.id, - }); - } - - await auditLog("medication.intake", { - userId: user.id, - ipAddress: getClientIp(request), - details: { - medicationId: id, - eventId: event.id, - skipped, + intakeSource, + isDelegated, + ipAddress: getClientIp(request), + originDeviceToken: request.headers.get("x-device-id"), }, - }); - - annotate({ - action: { - name: "medication.intake", - entity_type: "intake_event", - entity_id: event.id, - }, - meta: { - medication_id: id, - skipped, - }, - }); - - // v1.4.34 — bust per-user medications + compliance + achievement - // caches so the next read reflects the dose event. - invalidateUserMedications(user.id, { evict: true }); - - // v1.4.39 — refresh the persistent compliance rollup for the - // affected day. The hook is best-effort; failures annotate but never - // block the user's POST response. - await refreshDoseSlot({ - userId: user.id, - medicationId: id, - scheduledFor: event.scheduledFor, - tz: user.timezone, - }); - - // v1.5.0 — one-shot lifecycle reconciliation. A `oneShot` medication - // has at most one live intake; the helper re-reads the most recent - // non-skipped intake and flips `active` to match. Idempotent on - // non-one-shot medications (the underlying updateMany is gated by - // `oneShot:true`). The flip runs AFTER the intake row is committed - // so a flaky write never deactivates a medication that didn't - // actually receive its dose. - const reconcileAction = await reconcileOneShotState(prisma, id, user.id); - if (reconcileAction !== "noop") { - invalidateUserMedications(user.id, { evict: true }); + parsed.data, + ); + switch (outcome.kind) { + case "refused": + return apiError(outcome.message, 422, { errorCode: outcome.errorCode }); + case "existing": + return apiSuccess(outcome.row); + case "outcome_change_refused": + return refuseOutcomeChange(); + case "recorded": + return apiSuccess(outcome.event, 201); } - - // (#22) — silent cross-device intake sync: wake the user's OTHER iOS - // devices so a running Live Activity / widget reconciles this dose. - // Coalesced per user, best-effort — never affects the response. - queueMedicationIntakeSync({ - userId: user.id, - originDeviceToken: request.headers.get("x-device-id"), - }); - - // PWA counterpart of the sync wake above: a dose resolved here (taken or - // skipped) closes the still-pending dose-due Web Push reminder for the - // slot and refreshes the app badge. The other intake routes have carried - // this since v1.18.4; this route is the replay target of the iOS offline - // queue, so a drained dose must clear the web reminder the same way. - // Best-effort, fire-and-forget — the canonical row is already persisted. - void (async () => { - const badgeCount = await countOutstandingDosesToday(user.id, user.timezone); - await dispatchMedicationIntakeWebClear({ - userId: user.id, - medicationId: id, - scheduledFor: event.scheduledFor.toISOString(), - badgeCount, - }); - })(); - - // v1.36.x — "somebody else marked your dose". This route has no snooze arm, - // so the state is whichever of the two markings the payload carried. The - // helper refuses on self, so a person marking their own dose is unaffected. - // Fire and forget, like every neighbour above. The dispatcher awaits each - // channel in the cascade, so an unreachable one would have added its whole - // timeout to the caregiver's request — the person standing next to the - // patient waiting for a dose to register. The helper never throws, and the - // owner learning a moment later is the right trade against the tick being - // slow. The integration test already polls the ledger rather than relying on - // the await, so nothing about the proof weakens. - void notifyDelegatedIntake({ - ownerId: user.id, - actorId: actor.id, - medicationId: id, - status: skipped ? "skipped" : "taken", - }); - - return apiSuccess(event, 201); } export const GET = apiHandler( @@ -665,28 +158,28 @@ export const GET = apiHandler( const { user } = await requireRecordAuth("read", "medications"); const { id } = await params; - // v1.4.25 — privacy gate hoisted to the shared helper. + // Privacy gate through the shared helper. const guard = await assertMedicationOwnership(id, user.id); if (guard) return guard; const searchParams = Object.fromEntries(request.nextUrl.searchParams); const parsed = listIntakeEventsSchema.safeParse(searchParams); if (!parsed.success) { - // v1.4.43 — multi-issue 422 + audit breadcrumb keyed + // Multi-issue 422 + audit breadcrumb keyed // `medications.intake.list.validation-failed`. const issues = sanitiseZodIssues(parsed.error.issues); annotate({ action: { name: "medications.intake.list.validation-failed" }, meta: { issue_count: issues.length, medication_id: id }, }); - // v1.4.49 — strip `message` from the audit-ledger row. + // Strip `message` from the audit-ledger row. const auditIssues = sanitiseZodIssues(parsed.error.issues, { stripValuesFromMessage: true, }); - // v1.36.0 — through `auditLog()` rather than a bare `prisma.auditLog - // .create`, because that helper is the only thing that stamps - // `actorUserId`. Filed under the resolved record either way; without - // the stamp a delegate's malformed query would read as the owner's own. + // Through `auditLog()` rather than a bare `prisma.auditLog.create`, + // because that helper is the only thing that stamps `actorUserId`. + // Filed under the resolved record either way; without the stamp a + // delegate's malformed query would read as the owner's own. void auditLog("medications.intake.list.validation-failed", { userId: user.id, details: { issues: auditIssues, medicationId: id }, @@ -696,72 +189,12 @@ export const GET = apiHandler( return returnAllZodIssues(parsed.error, 422); } - const { limit, offset, sortBy, sortDir, status } = parsed.data; - - // v1.4.37 — translate the optional `status` filter into a Prisma - // `where` fragment. Default `status:"all"` keeps the contract - // byte-stable for the iOS Swift client and the dashboard tiles that - // were on the wire before this knob existed. The detail-page - // IntakeHistoryListV2 component opts into `status:"completed"` so - // ambiguous "missed / never confirmed" rows - // (`takenAt IS NULL AND skipped = false`) stay out of the user-facing - // table — they were the source of the v1.4.36 regression where rows - // with no takenAt rendered an "Eingenommen" chip. - const statusFilter = - status === "taken" - ? { takenAt: { not: null }, skipped: false } - : status === "skipped" - ? { skipped: true } - : status === "completed" - ? { - OR: [ - { takenAt: { not: null }, skipped: false }, - { skipped: true }, - ], - } - : {}; - // v1.7.0 sync — exclude tombstoned rows from the per-medication - // intake history list + its count. - const where = { - medicationId: id, - userId: user.id, - deletedAt: null, - ...statusFilter, - }; - - // v1.7.0 — pin NULLS LAST on the `takenAt` sort. Skipped rows - // carry `takenAt: null`; under a bare `desc` collation Postgres - // emits NULLS FIRST, floating skipped/planned rows to the top of - // the history view. Pinning them last keeps the descending order - // reading today → yesterday → … with real timestamps first. Other - // sort columns are non-null so they keep the simple shape. - // The trailing `{ id: sortDir }` is the unique tiebreaker. Scheduled - // slots share an instant by construction — a twice-daily medication puts - // every morning dose on the same `scheduledFor` — so offset paging over - // the primary key alone can repeat a row on one page and drop another. - const orderBy = - sortBy === "takenAt" - ? [ - { takenAt: { sort: sortDir, nulls: "last" as const } }, - { id: sortDir }, - ] - : [{ [sortBy]: sortDir }, { id: sortDir }]; - - const [events, total] = await Promise.all([ - prisma.medicationIntakeEvent.findMany({ - where, - orderBy, - take: limit, - skip: offset, + return apiSuccess( + await listIntakeEvents({ + userId: user.id, + medicationId: id, + query: parsed.data, }), - prisma.medicationIntakeEvent.count({ where }), - ]); - - annotate({ - action: { name: "medication.intake.list" }, - meta: { medication_id: id, total, status }, - }); - - return apiSuccess({ events, meta: { total, limit, offset } }); + ); }, ); diff --git a/src/app/api/medications/[id]/route.ts b/src/app/api/medications/[id]/route.ts index 211413d6a..7295d4532 100644 --- a/src/app/api/medications/[id]/route.ts +++ b/src/app/api/medications/[id]/route.ts @@ -1,330 +1,39 @@ -import { prisma } from "@/lib/db"; -import { sendsMedicationReminders } from "@/lib/medications/auto-mark-taken"; import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler"; -import { toIngredientDtos } from "@/lib/medications/ingredients/composition"; -import { - enqueueSupplementNutrientRecompute, - medicationHasComposition, -} from "@/lib/medications/ingredients/recompute-job"; -import { annotate } from "@/lib/logging/context"; -import { auditLog } from "@/lib/auth/audit"; -import { overwriteDetails } from "@/lib/sharing/audit-details"; import { apiSuccess, apiError, getClientIp } from "@/lib/api-response"; import { parseJsonBody } from "@/lib/api/body"; import { updateMedicationSchema } from "@/lib/validations/medication"; -import { invalidateUserMedications } from "@/lib/cache/invalidate"; -import { - deleteMedicationCategory, - isAssignableCategory, - resolveMedicationCategories, - setMedicationCategory, -} from "@/lib/medication-category"; -import { serializeScheduleRecurrence } from "@/lib/medication-schedule"; -import { - checkCurrentWindow, - CourseWriteError, - courseRefusalResponse, - resolveCourseFields, - setCurrentWindow, -} from "@/lib/medications/courses"; -import { - schedulesMateriallyDiffer, - toRevisionPayloadEntry, -} from "@/lib/medications/scheduling/schedule-eras"; -import type { Prisma } from "@/generated/prisma/client"; -import { - loadAutoTakeRollingAnchors, - moveAutoTakeFloorToNow, -} from "@/lib/medications/auto-take"; -import { - computeDisplayDue, - OVERDUE_LOOKBACK_MS, - toResolvedSlotMark, -} from "@/lib/medications/scheduling/next-due"; -import { - dayKeyForScheduledFor, - refreshDoseDay, -} from "@/lib/medications/dose-day-refresh"; import { assertMedicationOwnership } from "@/lib/medications/route-guards"; +import { readMedication } from "@/lib/medications/read-medication"; +import { findMedicationForEdit } from "@/lib/medications/update-medication/preflight"; +import { updateMedication } from "@/lib/medications/update-medication/update-medication"; import { - effectiveUnitsPerDose, - estimateUnitsRunwayDays, -} from "@/lib/medications/supply-runway"; -import { serializeScheduleUnitsPerDose } from "@/lib/medications/schedule-units-dto"; -import { hhmmToMinutesOrNull } from "@/lib/medications/scheduling/hhmm"; -import { getUserTodayBounds } from "@/lib/tz/local-day"; + deleteMedication, + findMedicationForDelete, +} from "@/lib/medications/delete-medication"; import { NextRequest } from "next/server"; -import { - dueSchedules, - isRecordOnly, - scheduleWireFields, -} from "@/lib/medications/intake-tracking"; -import { floorWhole } from "@/lib/medications/units-per-dose"; -import { resolveIntakeActionability } from "@/lib/medications/intake-actionable"; -import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; -import { buildRelated } from "@/lib/links/related"; type RouteParams = { params: Promise<{ id: string }> }; -/** Parse "HH:mm" into minutes-of-day; NaN-safe (malformed → 0). */ -function hhmmToMinutes(value: string): number { - return hhmmToMinutesOrNull(value) ?? 0; -} - -/** - * Reconcile a schedule's `windowStart` / `windowEnd` with its effective - * `timesOfDay`. The schedule editor can change only the dose times while the - * client echoes the previous window back; a time outside the window then - * breaks every consumer that assumes `timesOfDay ⊆ [windowStart, windowEnd]` - * (the window-status pill, the reminder phases, the legacy compliance - * walker). When ANY time falls outside the window — including the overnight - * `windowEnd < windowStart` shape, checked with wrap-around membership — - * the window is pulled to the min/max of the times so the persisted row is - * self-consistent. A window that already covers every time is left - * byte-identical (explicitly wider windows are a feature). - */ -function reconcileScheduleWindow( - windowStart: string, - windowEnd: string, - timesOfDay: string[], -): { windowStart: string; windowEnd: string } { - if (timesOfDay.length === 0) return { windowStart, windowEnd }; - const startMins = hhmmToMinutes(windowStart); - const endMins = hhmmToMinutes(windowEnd); - const overnight = endMins < startMins; - const inWindow = (t: number): boolean => - overnight ? t >= startMins || t <= endMins : t >= startMins && t <= endMins; - const sorted = [...timesOfDay].sort( - (a, b) => hhmmToMinutes(a) - hhmmToMinutes(b), - ); - const allInside = sorted.every((t) => inWindow(hhmmToMinutes(t))); - if (allInside) return { windowStart, windowEnd }; - return { windowStart: sorted[0], windowEnd: sorted[sorted.length - 1] }; -} - export const GET = apiHandler( async (_request: NextRequest, { params }: RouteParams) => { const { user, grantId } = await requireRecordAuth("read", "medications"); const { id } = await params; - const medication = await prisma.medication.findUnique({ - where: { id }, - include: { schedules: true }, - }); - - if (!medication || medication.userId !== user.id) { + const body = await readMedication({ user, grantId, id }); + if (!body) { return apiError("Medication not found", 404); } - let category = "OTHER"; - let categoryLabel: string | null = null; - try { - const categories = await resolveMedicationCategories([id]); - category = categories[id]?.category ?? "OTHER"; - categoryLabel = categories[id]?.categoryLabel ?? null; - } catch { - // Category enrichment is optional - } - - // v1.7.0 — server-computed next due instant. Rolling - // cadences re-anchor on the latest non-skipped intake, so fetch it - // once (no-op for calendar cadences but cheap + keeps the value - // correct for rolling injections). - const lastIntake = dueSchedules(medication).some( - (s) => s.rollingIntervalDays !== null, - ) - ? await prisma.medicationIntakeEvent.findFirst({ - // v1.7.0 sync — a tombstoned intake no longer anchors the - // rolling-interval next-due computation. - where: { - userId: user.id, - medicationId: id, - deletedAt: null, - takenAt: { not: null }, - }, - orderBy: { takenAt: "desc" }, - select: { takenAt: true }, - }) - : null; - // v1.15.10 — slots the user has already acted on near now, so the - // next-due search skips them and advances to the next genuinely-open - // slot. Bound the read to [now-1d, now+2d] — the lookahead only needs the - // slots adjacent to now. - const now = new Date(); - // v1.16.4 — reach back as far as the widest band tail so a - // long-resolved past slot can never resurface as "overdue". - const resolvedFrom = new Date(now.getTime() - OVERDUE_LOOKBACK_MS); - const resolvedTo = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000); - const resolvedRows = await prisma.medicationIntakeEvent.findMany({ - where: { - userId: user.id, - medicationId: id, - deletedAt: null, - scheduledFor: { gte: resolvedFrom, lte: resolvedTo }, - OR: [ - { takenAt: { not: null } }, - { skipped: true }, - { autoMissed: true }, - ], - }, - // v1.16.9 — `takenAt` rides along so an ad-hoc row (`scheduledFor - // === takenAt`) cannot ±6h-resolve a different slot. - select: { scheduledFor: true, takenAt: true }, - }); - // v1.16.4 — current-era floor: the open-overdue search mints from - // the LIVE schedule rows, so it must not reach past the newest - // revision boundary into a previous era's cadence. - const latestRevision = await prisma.medicationScheduleRevision.findFirst({ - // Superseded rows are audit records — a correction may have - // shortened the era, so the boundary reads only active rows. - where: { medicationId: id, supersededByRevisionId: null }, - orderBy: { validUntil: "desc" }, - select: { validUntil: true }, - }); - // v1.39.1 (#1033) — intake tracking off: nothing is due from the - // stored rows, and no runway derives from them. - const liveSchedules = dueSchedules(medication); - const userTz = user.timezone || DEFAULT_TIMEZONE; - // v1.42.2 — under auto-take a rolling cadence stands where the job will - // record next (see `auto-take.ts`). - const engineLastIntakeAt = await loadAutoTakeRollingAnchors(prisma, { - userId: user.id, - medicationIds: medication.autoMarkTaken ? [id] : [], - now, - userTz, - lastIntakeAt: () => lastIntake?.takenAt ?? null, - }); - const display = computeDisplayDue({ - medication: { - id: medication.id, - startsOn: medication.startsOn, - endsOn: medication.endsOn, - oneShot: medication.oneShot, - createdAt: medication.createdAt, - }, - schedules: liveSchedules, - now, - userTz, - lastIntakeAt: engineLastIntakeAt(id), - resolvedSlots: resolvedRows.map(toResolvedSlotMark), - eraStart: latestRevision?.validUntil ?? null, - }); - - annotate({ - action: { - name: "medication.get", - entity_type: "medication", - entity_id: id, - }, - }); - - // v1.37.19 — stock + runway on the detail wire, mirroring the list - // payload's semantics: NULL stock = inventory tracking off, 0 = the - // supply ran out; runwayDays via the slot-aware burn rate. Published - // resolved so no client re-derives the inheritance / cadence math. - const [usableStock, anyItemCount] = await Promise.all([ - prisma.medicationInventoryItem.aggregate({ - where: { - userId: user.id, - medicationId: id, - state: { in: ["ACTIVE", "IN_USE"] }, - unitsRemaining: { gt: 0 }, - }, - _sum: { unitsRemaining: true }, - }), - prisma.medicationInventoryItem.count({ - where: { userId: user.id, medicationId: id }, - }), - ]); - const schedulesDto = serializeScheduleUnitsPerDose( - medication.schedules, - medication.unitsPerDose, - ); - const stockUnitsRemaining = - anyItemCount > 0 ? Number(usableStock._sum.unitsRemaining ?? 0) : null; - const stockDosesRemaining = - stockUnitsRemaining === null - ? null - : floorWhole( - stockUnitsRemaining / - effectiveUnitsPerDose( - schedulesDto, - Number(medication.unitsPerDose), - ), - ); - const runwayDays = - stockUnitsRemaining === null - ? null - : estimateUnitsRunwayDays( - stockUnitsRemaining, - isRecordOnly(medication) ? [] : schedulesDto, - Number(medication.unitsPerDose), - ); - - // v1.43 — the composition per unit, labelled in the account language. - const ingredientRows = await prisma.medicationIngredient.findMany({ - where: { medicationId: id, userId: user.id }, - select: { - nutrient: true, - amountPerUnit: true, - unit: true, - enteredAmount: true, - enteredUnit: true, - }, - }); - - // v1.40 (#1024) — the courses, resolved on the server. - const courseFields = ( - await resolveCourseFields( - [medication], - now, - user.timezone || DEFAULT_TIMEZONE, - ) - ).get(id); - - return apiSuccess({ - ...medication, - unitsPerDose: Number(medication.unitsPerDose), - // v1.39.1 (#1033) — `schedules: []` plus `recordedSchedules` when - // intake tracking is off (see `scheduleWireFields`). - ...scheduleWireFields(medication.trackIntake, schedulesDto), - category, - categoryLabel, - ...courseFields, - nextDueAt: display ? display.at.toISOString() : null, - // v1.42.2 — the server's own reminder decision, published resolved. - remindersActive: sendsMedicationReminders(medication, { - now, - timeZone: userTz, - }), - nextDueOverdue: display?.overdue ?? false, - // v1.39.4 (#1040) — see `resolveIntakeActionability`. - ...resolveIntakeActionability( - medication, - now, - user.timezone || DEFAULT_TIMEZONE, - ), - stockUnitsRemaining, - stockDosesRemaining, - runwayDays, - ingredients: toIngredientDtos(ingredientRows, user.locale ?? null), - // v1.43 — the records this medication relates to (`src/lib/links/`). - related: await buildRelated(prisma, { - userId: user.id, - grantId, - kind: "medication", - id, - }), - }); + return apiSuccess(body); }, ); export const PUT = apiHandler( async (request: NextRequest, { params }: RouteParams) => { - // v1.37.0 — MANAGE, and the strongest candidate in the domain: a manager + // MANAGE, and the strongest candidate in the domain: a manager // administering somebody's regimen is the case the level exists for. What // it destroys is scheduling anchors, not history — the tombstone `where` - // below excludes every actioned row — and the previous cadence is the one + // excludes every actioned row — and the previous cadence is the one // genuinely unrecoverable part, which is what C7 puts in the audit row. // The DELETE beside it stays refused. const { user, authMethod } = await requireRecordAuth( @@ -333,36 +42,13 @@ export const PUT = apiHandler( ); const { id } = await params; - // v1.5.5 — route ownership check through the shared helper - // so the 404 leak shape stays identical across every - // `[id]/**` handler. The PUT branch still needs `existing.active` - // for the pausedAt-derivation step below, so re-read the narrow - // field after the guard passes. + // Route ownership check through the shared helper so the 404 leak shape + // stays identical across every `[id]/**` handler. The edit still needs + // `existing.active` for the pausedAt derivation, so it re-reads the + // narrow pre-image after the guard passes. const guard = await assertMedicationOwnership(id, user.id); if (guard) return guard; - const existing = await prisma.medication.findUnique({ - where: { id }, - // v1.16.11 — `asNeeded` + the schedule count feed the as-needed - // invariants below: a flip to as-needed must end schedule-less, - // a flip back to scheduled must end with at least one schedule. - select: { - active: true, - createdAt: true, - asNeeded: true, - // v1.39.1 (#1033) — the tracking transition below needs the before. - trackIntake: true, - // v1.42.2 — `autoMarkTakenSince` is stamped only on an off→on flip. - autoMarkTaken: true, - // v1.37.0 — the pre-image the audit row carries (C4). - name: true, - dose: true, - endsOn: true, - // v1.40 (#1024) — the course check below needs the stored window. - startsOn: true, - oneShot: true, - _count: { select: { schedules: true } }, - }, - }); + const existing = await findMedicationForEdit(id); if (!existing) { return apiError("Medication not found", 404); } @@ -372,839 +58,19 @@ export const PUT = apiHandler( }); if (!parsed.ok) return parsed.error; - // ── v1.39.1 (#1033) — a record-only schedule is only replaced knowingly ── - // - // A medication with intake tracking off is served with `schedules: []` - // and its stored schedule in `recordedSchedules`, so a client that - // predates the field arms no reminder from it. The price is that such a - // client cannot SEE the stored schedule. The shipped iPhone app (1.0.3 / - // 1.0.4) opens a medication without schedule rows in its editor as - // "daily at 08:00"; a rename or dose edit sends no `schedules` (its - // editor diffs against that same prefill), but any touch of a schedule - // field sends a full replacement built from the 08:00 default plus - // `asNeeded` and `oneShot`, and never `trackIntake`. Applied, that would - // overwrite the schedule the person kept on record with one they never - // chose (or flip it to as-needed and delete the rows). - // - // Rule: on a medication whose intake tracking is off, a write that does - // not name `trackIntake` leaves the schedule shape alone. `schedules`, - // `asNeeded` and `oneShot` are dropped from it; every other field (name, - // dose, dates, `active`, `notificationsEnabled`, inventory, ...) applies - // as sent. A client that knows about the switch names it on every - // schedule save (the web editor and wizard do), and those writes apply. - const keepRecordedSchedule = - existing.trackIntake === false && - parsed.data.trackIntake === undefined && - (parsed.data.schedules !== undefined || - parsed.data.asNeeded !== undefined || - parsed.data.oneShot !== undefined); - const input = keepRecordedSchedule - ? { - ...parsed.data, - schedules: undefined, - asNeeded: undefined, - oneShot: undefined, - } - : parsed.data; - if (keepRecordedSchedule) { - annotate({ - action: { - name: "medication.update.record_schedule_kept", - entity_type: "medication", - entity_id: id, - }, - meta: { - dropped_schedules: parsed.data.schedules !== undefined, - dropped_as_needed: parsed.data.asNeeded !== undefined, - dropped_one_shot: parsed.data.oneShot !== undefined, - }, - }); - } - - const { - name, - dose, - category, - treatmentClass, - dosesPerUnit, - unitsPerDose, - reorderLeadDays, - deliveryForm, - trackInjectionSites, - allowedInjectionSites, - active, - notificationsEnabled, - liveActivityEnabled, - criticalAlarmEnabled, - atcCode, - rxNormCode, - schedules, - startsOn, - endsOn, - oneShot, - asNeeded, - trackIntake, - autoMarkTaken, - reminderGraceMinutes: topLevelGraceMinutes, - } = input; - - // v1.40 (#1041) — a custom category must be one of this record's own. - if ( - category !== undefined && - !(await isAssignableCategory(user.id, category)) - ) { - return apiError("Unknown medication category", 422, { - errorCode: "medications.category.unknown", - }); - } - - // v1.39.1 (#1033) — intake tracking transition. Off keeps the schedule - // as a record; the live era from here on expects nothing. - const wasTracked = existing.trackIntake !== false; - const willTrack = trackIntake ?? wasTracked; - const trackingChanged = wasTracked !== willTrack; - - // ── v1.16.11 (#316) — as-needed invariants ────────────────────── - // - // An as-needed medication ends schedule-less, always. The Zod layer - // already rejects a populated `schedules` array alongside - // `asNeeded: true`; the route covers the cross-row cases the schema - // cannot see: - // - flipping an existing scheduled medication to as-needed - // requires `schedules: []` in the same request (keeping the old - // rows alongside the flag is a 422, per the feature contract); - // - flipping as-needed OFF requires the medication to end with at - // least one schedule (the wizard always sends the full array). - const effectiveAsNeeded = asNeeded ?? existing.asNeeded; - if (effectiveAsNeeded) { - const endsScheduleless = schedules - ? schedules.length === 0 - : existing._count.schedules === 0; - if (!endsScheduleless) { - return apiError( - "An as-needed medication cannot carry schedules (send schedules: [] to clear them)", - 422, - ); - } - } else if ( - asNeeded === false && - existing.asNeeded && - (!schedules || schedules.length === 0) - ) { - return apiError( - "A scheduled medication requires at least one schedule", - 422, - ); - } - - // ── v1.5.5 — primary-schedule grace bridge ────────────────────── - // - // The detail-page settings section saves the reminder-window in - // one PUT carrying only `{ reminderGraceMinutes }` at the top - // level (no full `schedules` array). Map it onto the medication's - // primary schedule before the Prisma update so the persisted - // shape stays per-schedule and the engine reads it from the same - // column as the wizard write-path. A full `schedules` array on - // the same request takes precedence — the wizard already declares - // the grace per row and we never want to overwrite that intent. - let primaryScheduleGracePatch: { - scheduleId: string; - reminderGraceMinutes: number | null; - } | null = null; - if (topLevelGraceMinutes !== undefined && !schedules) { - const primary = await prisma.medicationSchedule.findFirst({ - where: { medicationId: id }, - orderBy: { windowStart: "asc" }, - select: { id: true }, - }); - if (primary) { - primaryScheduleGracePatch = { - scheduleId: primary.id, - reminderGraceMinutes: topLevelGraceMinutes, - }; - } - } - - // ── v1.5 route invariants for the new scheduling primitives ───── - // - // Mirrors the POST route: oneShot ⇒ at-most-one schedule + no - // recurrence on it; endsOn normalised to startsOn for one-shot; - // recurring default = FREQ=DAILY when nothing else is set; - // timesOfDay dual-write from windowStart when absent. - // - // The PUT replaces all schedules wholesale, so the invariants run - // against the incoming `schedules` array (if provided) the same - // way they do on create. - if (oneShot === true && schedules) { - if (schedules.length > 1) { - return apiError( - "A one-shot medication can have at most one schedule", - 422, - ); - } - const s = schedules[0]; - if (s && (s.rrule !== undefined || s.rollingIntervalDays !== undefined)) { - return apiError( - "A one-shot medication cannot have a recurrence (rrule or rollingIntervalDays)", - 422, - ); - } - } - - // Normalise endsOn for one-shot. `oneShot === true` + `startsOn` - // means the dose is the start date; endsOn auto-matches. - const normalisedEndsOn = oneShot === true && startsOn ? startsOn : endsOn; - - // v1.40 (#1024) — the window a PUT sets is the latest course. Check it - // against the other courses BEFORE the row changes, so a refused window - // leaves the medication as it was. - const touchesWindow = - startsOn !== undefined || - normalisedEndsOn !== undefined || - oneShot !== undefined; - const nextWindow = { - startsOn: startsOn !== undefined ? startsOn : existing.startsOn, - endsOn: - normalisedEndsOn !== undefined ? normalisedEndsOn : existing.endsOn, - }; - const courseTz = user.timezone || DEFAULT_TIMEZONE; - if (touchesWindow) { - const refusal = await checkCurrentWindow({ - userId: user.id, - medicationId: id, - timeZone: courseTz, - ...nextWindow, - oneShot: oneShot ?? existing.oneShot, - createdAt: existing.createdAt, - }); - if (refusal) { - const r = courseRefusalResponse(refusal); - return apiError(r.message, r.status, { errorCode: r.errorCode }); - } - } - - const pausedAtPatch = - active === undefined - ? {} - : active - ? { pausedAt: null as Date | null } - : existing.active - ? { pausedAt: new Date() } - : {}; - - // v1.16.3 — normalise the incoming schedules ONCE: the same values feed - // the create payload below AND the revision compare/snapshot (so the - // material-change gate sees exactly what will be persisted). - const normalisedSchedules = schedules?.map((s) => { - // Invariant 2 — default to FREQ=DAILY when nothing else is set. - // v1.7.0 — PRN carries no cadence, so never default it. - const hasLegacyDays = (s.daysOfWeek?.length ?? 0) > 0; - const isPrn = s.scheduleType === "PRN"; - const defaultedRrule = - oneShot !== true && - !isPrn && - s.rrule === undefined && - s.rollingIntervalDays === undefined && - !hasLegacyDays - ? "FREQ=DAILY" - : s.rrule; - - // Invariant 3 — dual-write timesOfDay from windowStart. - const effectiveTimesOfDay = - s.timesOfDay && s.timesOfDay.length > 0 - ? s.timesOfDay - : [s.windowStart]; - - // Invariant 4 — window / times consistency. See - // `reconcileScheduleWindow`: a times-only edit that echoes the - // stale window back gets the window pulled to the min/max of - // the new times. - const window = reconcileScheduleWindow( - s.windowStart, - s.windowEnd, - effectiveTimesOfDay, - ); - - const serializedDaysOfWeek = serializeScheduleRecurrence({ - daysOfWeek: s.daysOfWeek ?? [], - intervalWeeks: s.intervalWeeks ?? 1, - }); - - return { - createData: { - windowStart: window.windowStart, - windowEnd: window.windowEnd, - label: s.label ?? null, - dose: s.dose ?? null, - // #219 — per-schedule inventory units, field-by-field. A schedule - // replace re-creates the rows, so absent leaves the column NULL - // and the consume hook inherits the medication-level value. - ...(s.unitsPerDose !== undefined && { - unitsPerDose: s.unitsPerDose, - }), - daysOfWeek: serializedDaysOfWeek, - // v1.5 first-class times-of-day. - timesOfDay: effectiveTimesOfDay, - ...(s.reminderGraceMinutes !== undefined && { - reminderGraceMinutes: s.reminderGraceMinutes, - }), - ...(defaultedRrule !== undefined && { rrule: defaultedRrule }), - ...(s.rollingIntervalDays !== undefined && { - rollingIntervalDays: s.rollingIntervalDays, - }), - // v1.7.0 — schedule type + cyclic weeks, field-by-field. - ...(s.scheduleType !== undefined && { - scheduleType: s.scheduleType, - }), - ...(s.cyclicOnWeeks !== undefined && { - cyclicOnWeeks: s.cyclicOnWeeks, - }), - ...(s.cyclicOffWeeks !== undefined && { - cyclicOffWeeks: s.cyclicOffWeeks, - }), - // v1.15.18 — per-dose configurable on-time windows. A `schedules` - // replace re-creates the rows, so the column is re-written each - // time; absent leaves it NULL (default ±1h derivation). - ...(s.doseWindows !== undefined && { - doseWindows: s.doseWindows, - }), - }, - // Mirror of the row the create above will persist (defaults applied), - // shaped for the cadence compare. - snapshot: toRevisionPayloadEntry({ - timesOfDay: effectiveTimesOfDay, - windowStart: window.windowStart, - windowEnd: window.windowEnd, - daysOfWeek: serializedDaysOfWeek, - rrule: defaultedRrule ?? null, - rollingIntervalDays: s.rollingIntervalDays ?? null, - scheduleType: s.scheduleType ?? "SCHEDULED", - cyclicOnWeeks: s.cyclicOnWeeks ?? null, - cyclicOffWeeks: s.cyclicOffWeeks ?? null, - doseWindows: s.doseWindows ?? null, - label: s.label ?? null, - dose: s.dose ?? null, - reminderGraceMinutes: s.reminderGraceMinutes ?? null, - }), - }; - }); - - // v1.37.0 — C7. What a schedule replacement destroys, captured for the - // audit row: the cadence that was there and the pending slots that were - // tombstoned with it. A feed line saying "changed a medication" cannot - // answer "back to what?", and unlike a row a schedule keeps ACTING after - // the manager's grant ends. - let replacedCadence: string | null = null; - let replacedScheduleCount: number | null = null; - let tombstonedSlotCount = null as number | null; - // Assigned inside `tombstoneOpenSlots`; the casts keep TypeScript from - // narrowing them to `null` across the closure. - let tombstonedFrom = null as Date | null; - let tombstonedTo = null as Date | null; - - // Tombstone today's and future open pending rows of this medication. - // Runs on a schedule replace (the old anchors are stale) and when intake - // tracking is switched off (nothing is due any more). - // The dose days the tombstoned slots sat on, refreshed once the - // transaction below has committed the tombstones. - const staleDoseDays = new Set(); - const tombstoneOpenSlots = async ( - db: Prisma.TransactionClient, - ): Promise => { - // A schedule replace invalidates the open slot anchors the projector / - // reminder worker minted for the OLD times: a pending 08:00 row for a - // medication that now doses at 20:00 would linger as a phantom slot, - // auto-miss later, and depress compliance. Tombstone today's and - // future open pending rows (never an actioned row — `takenAt`, - // `skipped`, and `autoMissed` are user-visible history) and bump - // `syncVersion` so delta-sync clients drop them. The projector and - // the reminder worker re-mint the anchors for the new times on their - // next pass — the route does not pre-create them. - const userTz = user.timezone || DEFAULT_TIMEZONE; - const { start: todayStart } = getUserTodayBounds(new Date(), userTz); - const tombstoneWhere = { - userId: user.id, - medicationId: id, - deletedAt: null, - takenAt: null, - skipped: false, - autoMissed: false, - scheduledFor: { gte: todayStart }, - } as const; - // Capture the affected slots BEFORE the tombstone so the compliance - // rollups for those days can be recomputed after the commit — - // `updateMany` does not return the touched rows. Without the - // recompute a tombstoned pending kept counting as `scheduled` in - // the rollup row until the next event-driven write for that day, - // depressing the rate the cards read in the meantime. - const tombstoned = await db.medicationIntakeEvent.findMany({ - where: tombstoneWhere, - select: { scheduledFor: true }, - }); - // The count and the day range of the anchors that went. - tombstonedSlotCount = tombstoned.length; - for (const row of tombstoned) { - if (tombstonedFrom === null || row.scheduledFor < tombstonedFrom) { - tombstonedFrom = row.scheduledFor; - } - if (tombstonedTo === null || row.scheduledFor > tombstonedTo) { - tombstonedTo = row.scheduledFor; - } - } - await db.medicationIntakeEvent.updateMany({ - where: tombstoneWhere, - data: { deletedAt: new Date(), syncVersion: { increment: 1 } }, - }); - // Deduped via the user-tz day key. - for (const row of tombstoned) { - staleDoseDays.add(dayKeyForScheduledFor(row.scheduledFor, userTz)); - } - }; - - // v1.40 — everything the PUT writes to the medication, its schedule and - // its course commits as one transaction. The course write can still be - // refused after the check above (another request wrote a course in - // between); with the row update committed first, the client got the - // refusal while the name, the schedule and `oneShot` were already - // changed. Now a refusal rolls all of it back. The rollup recompute and - // the grace bridge stay outside: both read or write what this commit - // settles, and neither may undo it. - let medication; - try { - medication = await prisma.$transaction(async (tx) => { - // If schedules provided, replace all - if (schedules && normalisedSchedules) { - // v1.16.3 — effective dating. Before the wholesale replace below wipes - // the old rows, archive them as ONE revision covering - // `[previous validUntil | medication.createdAt, now)` — but only when a - // cadence-relevant field actually changes. A no-op edit (the Zeitplan - // tab echoing the same rows back) must not mint a phantom era. - const previousRows = await tx.medicationSchedule.findMany({ - where: { medicationId: id }, - }); - const previousEntries = previousRows.map((row) => - toRevisionPayloadEntry({ - timesOfDay: row.timesOfDay, - windowStart: row.windowStart, - windowEnd: row.windowEnd, - daysOfWeek: row.daysOfWeek, - rrule: row.rrule, - rollingIntervalDays: row.rollingIntervalDays, - scheduleType: row.scheduleType, - cyclicOnWeeks: row.cyclicOnWeeks, - cyclicOffWeeks: row.cyclicOffWeeks, - doseWindows: row.doseWindows, - label: row.label, - dose: row.dose, - reminderGraceMinutes: row.reminderGraceMinutes, - }), - ); - replacedScheduleCount = previousRows.length; - replacedCadence = - previousRows.length > 0 - ? previousRows - .map((row) => - [ - row.timesOfDay, - row.rrule ?? "", - row.rollingIntervalDays !== null - ? `every ${row.rollingIntervalDays}d` - : "", - row.daysOfWeek ?? "", - ] - .filter((part) => part !== "") - .join(" "), - ) - .join(" | ") - : null; - - // v1.39.1 (#1033) — a stretch with intake tracking off expected - // nothing, so it archives with an empty payload whatever the rows - // said; and a tracking transition is itself an era boundary even when - // the rows stay the same. - const archivedEntries = wasTracked ? previousEntries : []; - if ( - previousRows.length > 0 && - (trackingChanged || - !wasTracked || - schedulesMateriallyDiffer( - previousEntries, - normalisedSchedules.map((n) => n.snapshot), - )) - ) { - const lastRevision = await tx.medicationScheduleRevision.findFirst({ - // Chain from the ACTIVE boundary: a superseded row is an - // audit record whose `validUntil` may sit past its correction. - where: { medicationId: id, supersededByRevisionId: null }, - orderBy: { validUntil: "desc" }, - select: { validUntil: true }, - }); - await tx.medicationScheduleRevision.create({ - data: { - medicationId: id, - validFrom: lastRevision?.validUntil ?? existing.createdAt, - validUntil: new Date(), - payload: archivedEntries as unknown as Prisma.InputJsonValue, - }, - }); - annotate({ - action: { - name: "medication.schedule.revision_archived", - entity_type: "medication", - entity_id: id, - }, - meta: { schedule_revision_rows: archivedEntries.length }, - }); - } else if ( - previousRows.length === 0 && - existing.asNeeded && - asNeeded === false && - normalisedSchedules.length > 0 - ) { - // v1.16.11 — flipping as-needed OFF. The medication carried ZERO - // schedule rows while as-needed, so the wholesale-replace archive - // above never fires — and without a revision the live era would - // start at the PREVIOUS revision's `validUntil` (or `createdAt`), - // retro-painting the schedule-less as-needed stretch with the NEW - // schedule's expected slots: every PRN day would read as missed. - // Archive an EMPTY era covering the as-needed stretch instead; an - // empty payload expands to zero schedules, so era-aware compliance - // expects nothing there — exactly the as-needed contract. - const lastRevision = await tx.medicationScheduleRevision.findFirst({ - where: { medicationId: id, supersededByRevisionId: null }, - orderBy: { validUntil: "desc" }, - select: { validUntil: true }, - }); - await tx.medicationScheduleRevision.create({ - data: { - medicationId: id, - validFrom: lastRevision?.validUntil ?? existing.createdAt, - validUntil: new Date(), - payload: [] as unknown as Prisma.InputJsonValue, - }, - }); - annotate({ - action: { - name: "medication.schedule.revision_archived", - entity_type: "medication", - entity_id: id, - }, - meta: { schedule_revision_rows: 0 }, - }); - } - await tombstoneOpenSlots(tx); - await tx.medicationSchedule.deleteMany({ - where: { medicationId: id }, - }); - } else if (trackingChanged) { - // v1.39.1 (#1033) — intake tracking switched without a schedule - // replace. Close the current era at this instant: switching OFF - // archives the schedule that was in force (the tracked stretch keeps - // its expected doses); switching back ON archives the untracked - // stretch with an empty payload, so compliance never counts it as - // missed and the live schedule, the projector and the reminder worker - // all resume from now. - const liveRows = await tx.medicationSchedule.findMany({ - where: { medicationId: id }, - }); - if (liveRows.length > 0) { - const lastRevision = await tx.medicationScheduleRevision.findFirst({ - where: { medicationId: id, supersededByRevisionId: null }, - orderBy: { validUntil: "desc" }, - select: { validUntil: true }, - }); - const archivedEntries = wasTracked - ? liveRows.map((row) => - toRevisionPayloadEntry({ - timesOfDay: row.timesOfDay, - windowStart: row.windowStart, - windowEnd: row.windowEnd, - daysOfWeek: row.daysOfWeek, - rrule: row.rrule, - rollingIntervalDays: row.rollingIntervalDays, - scheduleType: row.scheduleType, - cyclicOnWeeks: row.cyclicOnWeeks, - cyclicOffWeeks: row.cyclicOffWeeks, - doseWindows: row.doseWindows, - label: row.label, - dose: row.dose, - reminderGraceMinutes: row.reminderGraceMinutes, - }), - ) - : []; - await tx.medicationScheduleRevision.create({ - data: { - medicationId: id, - validFrom: lastRevision?.validUntil ?? existing.createdAt, - validUntil: new Date(), - payload: archivedEntries as unknown as Prisma.InputJsonValue, - }, - }); - annotate({ - action: { - name: "medication.schedule.revision_archived", - entity_type: "medication", - entity_id: id, - }, - meta: { - schedule_revision_rows: archivedEntries.length, - track_intake: willTrack, - }, - }); - } - } - - // v1.39.1 (#1033) — switching intake tracking off leaves nothing due: - // the open placeholders of today and later go the way a schedule - // replace sends them (a replace in the same request already did it). - if (trackingChanged && !willTrack && !schedules) { - await tombstoneOpenSlots(tx); - } - - const baseUpdateData = { - ...(name !== undefined && { name }), - ...(dose !== undefined && { dose }), - ...(treatmentClass !== undefined && { treatmentClass }), - ...(dosesPerUnit !== undefined && { dosesPerUnit }), - // v1.16.10 — units consumed per dose. Field-by-field; the stamp on - // already-taken intake events freezes their recorded consumption. - ...(unitsPerDose !== undefined && { unitsPerDose }), - // v1.17.0 — reorder lead override. Field-by-field; `null` is a - // valid explicit value (clears the override → user-level default). - ...(reorderLeadDays !== undefined && { reorderLeadDays }), - ...(deliveryForm !== undefined && { deliveryForm }), - // v1.8.5 — injection-site tracking opt-in + per-medication allowed - // sites. Field-by-field; `false` / `[]` are valid explicit values - // (deactivate tracking / clear the per-med restriction). - ...(trackInjectionSites !== undefined && { trackInjectionSites }), - ...(allowedInjectionSites !== undefined && { allowedInjectionSites }), - ...(active !== undefined && { active }), - ...(notificationsEnabled !== undefined && { notificationsEnabled }), - // v1.7.0 — iOS reminder flags, field-by-field. - ...(liveActivityEnabled !== undefined && { liveActivityEnabled }), - ...(criticalAlarmEnabled !== undefined && { criticalAlarmEnabled }), - // v1.9.0 — optional drug-classification codes (ATC / RxNorm). - // Field-by-field; `null` is a valid explicit value (clears the code). - ...(atcCode !== undefined && { atcCode }), - ...(rxNormCode !== undefined && { rxNormCode }), - // v1.5 scheduling primitives — pass-through when supplied. - // `startsOn` / `endsOn` are `Date | null | undefined` (the - // schema lets the user clear them explicitly with null). - // v1.40 (#1024) — `startsOn` / `endsOn` are not written here: the - // course writer below writes them together with the course they - // project, in one transaction, so the row and its courses cannot - // disagree if one of the two writes fails. - ...(oneShot !== undefined && { oneShot }), - // v1.16.11 — as-needed flag, field-by-field (the invariants above - // already guaranteed the medication ends schedule-consistent). - ...(asNeeded !== undefined && { asNeeded }), - // v1.39.1 (#1033) — intake tracking, field-by-field. - ...(trackIntake !== undefined && { trackIntake }), - // v1.42.2 — auto-mark-taken, field-by-field. Only an off→on flip - // stamps `autoMarkTakenSince`, so re-saving the form with the - // option already on never moves the floor; switching it off - // clears the stamp. - ...(autoMarkTaken !== undefined && - autoMarkTaken !== existing.autoMarkTaken && { - autoMarkTaken, - autoMarkTakenSince: autoMarkTaken ? new Date() : null, - }), - ...(normalisedSchedules && { - schedules: { - create: normalisedSchedules.map((n) => n.createData), - }, - }), - }; - - const updated = await tx.medication.update({ - where: { id }, - data: { ...baseUpdateData, ...pausedAtPatch }, - include: { schedules: true }, - }); - - // v1.25 — durable pause intervals. The `pausedAt` column is a - // single live marker that resume clears, so the paused window is - // irrecoverable once resumed and every paused day's slot collapses to - // "missed" in the compliance denominator. Mirror the live transition - // onto an additive era record the rate paths read: a pause opens an era - // (resumedAt null), a resume closes the latest open era. `userId` comes - // from the authenticated session; the data object is built field-by-field. - if (active === false && existing.active) { - await tx.medicationPauseEra.create({ - data: { - medicationId: id, - userId: user.id, - pausedAt: new Date(), - resumedAt: null, - }, - }); - } else if (active === true && !existing.active) { - await tx.medicationPauseEra.updateMany({ - where: { medicationId: id, userId: user.id, resumedAt: null }, - data: { resumedAt: new Date() }, - }); - // v1.42.2 — made active again: the auto-take pass starts from - // now, never filling the inactive days as taken. - await moveAutoTakeFloorToNow(tx, id); - } - - // v1.40 (#1024) — write the window onto the latest course and - // project it back, then serve the projected row. - if (touchesWindow) { - const projected = await setCurrentWindow( - { - userId: user.id, - medicationId: id, - timeZone: courseTz, - ...nextWindow, - }, - tx, - ); - return { ...updated, ...projected }; - } - return updated; - }); - } catch (err) { - // A concurrent course write landed between the check above and the - // course write. The transaction rolled back: nothing was written. - if (err instanceof CourseWriteError) { - const r = courseRefusalResponse(err.refusal); - return apiError(r.message, r.status, { errorCode: r.errorCode }); - } - throw err; - } - - // Refresh each dose day the tombstoned slots sat on, now that the - // tombstones are committed. Best-effort like every dose write-hook: a - // failure is reported, never fails the edit. - for (const dayKey of staleDoseDays) { - await refreshDoseDay({ - userId: user.id, - medicationId: id, - dayKey, - tz: user.timezone || DEFAULT_TIMEZONE, - }); - } - - // Apply the v1.5.5 primary-schedule grace bridge after the - // medication update so a failure here doesn't roll the medication - // edit back. Re-read the row so the response shape carries the - // converged grace value. - if (primaryScheduleGracePatch) { - await prisma.medicationSchedule.update({ - where: { id: primaryScheduleGracePatch.scheduleId }, - data: { - reminderGraceMinutes: primaryScheduleGracePatch.reminderGraceMinutes, - }, - }); - const refreshed = await prisma.medication.findUnique({ - where: { id }, - include: { schedules: true }, - }); - if (refreshed) { - medication = refreshed; - } - } - - // v1.40 (#1041) — an iPhone app build that predates custom categories - // decodes an unknown category as OTHER and sends it back on every edit, - // which would overwrite the person's own category with OTHER. A native - // client (Bearer) sending OTHER over a custom category keeps the custom - // one; moving a custom category back to OTHER stays possible on the web. - let categoryToWrite = category; - if (category === "OTHER" && authMethod === "bearer") { - const current = (await resolveMedicationCategories([id]))[id]; - if (current?.category.startsWith("custom:")) { - categoryToWrite = undefined; - annotate({ - action: { name: "medication.update.custom_category_kept" }, - meta: { entity_id: id }, - }); - } - } - if (categoryToWrite !== undefined) - await setMedicationCategory(id, categoryToWrite); - const resolvedCategory = (await resolveMedicationCategories([id]))[id]; - const normalizedCategory = resolvedCategory?.category ?? "OTHER"; - - // v1.43 — a changed units-per-dose (on the medication or on a slot) - // changes what every taken dose added, so a medication with a composition - // has its doses recomputed. Cheap and coalesced when nothing moved. - if (await medicationHasComposition(user.id, id)) { - await enqueueSupplementNutrientRecompute(user.id); - } - - await auditLog("medication.update", { - userId: user.id, + const outcome = await updateMedication({ + user, + authMethod, + id, + existing, + data: parsed.data, ipAddress: getClientIp(request), - // C4 on what the medication row itself lost, and C7 on the schedule: - // the cadence that was replaced, and how many pending slots went with - // it over which days. - details: { - medicationId: id, - ...overwriteDetails({ - before: { - name: existing.name, - dose: existing.dose, - active: existing.active, - asNeeded: existing.asNeeded, - trackIntake: existing.trackIntake, - // v1.42.2 — switching automatic recording on or off is a change - // to how doses are recorded, so the row names it. - autoMarkTaken: existing.autoMarkTaken, - endsOn: existing.endsOn, - }, - after: { - name: medication.name, - dose: medication.dose, - active: medication.active, - asNeeded: medication.asNeeded, - trackIntake: medication.trackIntake, - autoMarkTaken: medication.autoMarkTaken, - endsOn: medication.endsOn, - }, - }), - ...(replacedScheduleCount !== null - ? { - replacedCadence, - replacedScheduleCount, - tombstonedSlotCount, - tombstonedFrom: tombstonedFrom?.toISOString() ?? null, - tombstonedTo: tombstonedTo?.toISOString() ?? null, - } - : {}), - }, }); + if (!outcome.ok) { + return apiError(outcome.message, outcome.status, outcome.meta); + } - annotate({ - action: { - name: "medication.update", - entity_type: "medication", - entity_id: id, - }, - }); - - // v1.4.34 — bust per-user medications + compliance + achievement - // caches so the next read reflects the schedule change. - invalidateUserMedications(user.id, { evict: true }); - - return apiSuccess({ - ...medication, - unitsPerDose: Number(medication.unitsPerDose), - ...scheduleWireFields( - medication.trackIntake, - serializeScheduleUnitsPerDose( - medication.schedules, - medication.unitsPerDose, - ), - ), - category: normalizedCategory, - categoryLabel: resolvedCategory?.categoryLabel ?? null, - // v1.40 (#1024) — the courses, as every medication read carries them. - ...(await resolveCourseFields([medication], new Date(), courseTz)).get( - id, - ), - }); + return apiSuccess(outcome.body); }, ); @@ -1213,58 +79,22 @@ export const DELETE = apiHandler( const { user } = await requireAuth(); const { id } = await params; - // v1.5.5 — ownership via the shared helper. The DELETE - // branch needs the medication name for the audit-row detail; pull - // it after the guard passes. + // Ownership via the shared helper. The delete needs the medication name + // for the audit-row detail; it is read after the guard passes. const guard = await assertMedicationOwnership(id, user.id); if (guard) return guard; - const existing = await prisma.medication.findUnique({ - where: { id }, - select: { name: true }, - }); + const existing = await findMedicationForDelete(id); if (!existing) { return apiError("Medication not found", 404); } - // Revoke API tokens scoped to this medication - const medicationScope = `medication:${id}:ingest`; - await prisma.apiToken.updateMany({ - where: { - userId: user.id, - revoked: false, - permissions: { has: medicationScope }, - }, - data: { revoked: true }, - }); - - // v1.43 — whether its doses counted toward the micronutrients, read - // before the delete cascades the composition away. - const hadComposition = await medicationHasComposition(user.id, id); - - await deleteMedicationCategory(id); - await prisma.medication.delete({ where: { id } }); - - await auditLog("medication.delete", { + await deleteMedication({ userId: user.id, + id, + existing, ipAddress: getClientIp(request), - details: { medicationId: id, name: existing.name }, }); - annotate({ - action: { - name: "medication.delete", - entity_type: "medication", - entity_id: id, - }, - }); - - // v1.4.34 — bust per-user medications + compliance + achievement - // caches so the next read reflects the deletion. - invalidateUserMedications(user.id, { evict: true }); - - // Its doses are gone with it, so the days they counted on are recomputed. - if (hadComposition) await enqueueSupplementNutrientRecompute(user.id); - return apiSuccess({ deleted: true }); }, ); diff --git a/src/app/api/medications/intake/bulk/route.ts b/src/app/api/medications/intake/bulk/route.ts index 7e6f9a453..80d490cb6 100644 --- a/src/app/api/medications/intake/bulk/route.ts +++ b/src/app/api/medications/intake/bulk/route.ts @@ -45,7 +45,6 @@ import { z } from "zod/v4"; import { prisma } from "@/lib/db"; import { apiHandler, isScopedCredential, requireAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; -import { auditLog } from "@/lib/auth/audit"; import { apiError, apiSuccess, @@ -56,52 +55,13 @@ import { } from "@/lib/api-response"; import { withIdempotency } from "@/lib/idempotency"; import { checkRateLimit } from "@/lib/rate-limit"; -import { markSyncCheckpoint } from "@/lib/sync/checkpoint"; -import { - BACKFILL_SYNC_TRIGGER, - HEALTHKIT_REQUEST_SYNC_TRIGGERS, - MEDICATION_DOSE_ARRIVAL_TYPE, - recordHealthKitTypeArrivals, -} from "@/lib/integrations/healthkit-type-sync"; -import { invalidateUserMedications } from "@/lib/cache/invalidate"; -import { - dayKeyForScheduledFor, - refreshDoseDay, -} from "@/lib/medications/dose-day-refresh"; -import { - applyCanonicalSlotWrite, - findPinConflict, - mayConvergeOntoSuppliedSlot, - resolveForcedSlotForWrite, - resolveSlotForWriteByBand, - resolveSlotInstantForWrite, - slotHeldByAnotherDose, -} from "@/lib/medications/scheduling/slot-upsert"; -import { resolveInjectionSiteForWrite } from "@/lib/medications/injection-site-write"; -import { - consumeForIntake, - restoreForIntake, -} from "@/lib/medications/inventory/consumption"; +import { HEALTHKIT_REQUEST_SYNC_TRIGGERS } from "@/lib/integrations/healthkit-type-sync"; import { boundedTakenAtSchema, injectionSiteEnum, - type InjectionSiteValue, } from "@/lib/validations/medication"; -import { - classifyExternalId, - unstableExternalIdMeta, - UNSTABLE_EXTERNAL_ID_REASON, - type UnstableExternalIdShape, -} from "@/lib/validations/external-id"; -import type { InjectionSiteKey } from "@/lib/medications/injection-sites"; -import { queueMedicationIntakeSync } from "@/lib/notifications/medication-intake-sync"; -import { dispatchMedicationIntakeWebClearBulk } from "@/lib/notifications/web-push-clear"; -import { countOutstandingDosesToday } from "@/lib/medications/outstanding-doses"; -import { - INTAKE_NOT_TRACKED_REASON, - isRecordOnly, -} from "@/lib/medications/intake-tracking"; import { isoDateTime } from "@/lib/validations/datetime"; +import { ingestBulkIntake } from "@/lib/medications/intake-bulk/ingest"; const MAX_ENTRIES_PER_BATCH = 500; const BATCH_RATE_LIMIT_MAX = 60; @@ -121,7 +81,7 @@ const bulkEntrySchema = z // equality forever, so it is an identity and carries the same // stability floor. Checked per entry for the same reason as // `externalId` above. - // @external-id-checked-per-entry: src/app/api/medications/intake/bulk/route.ts + // @external-id-checked-per-entry: src/lib/medications/intake-bulk/entry.ts idempotencyKey: z.string().min(1).max(128).optional(), // v1.8.5 — optional per-entry injection site. Validated against the // medication's effective allowed set; a disallowed site marks the @@ -157,7 +117,7 @@ const bulkEntrySchema = z // Stability is enforced per entry in the loop below, not at the // field: a schema-level refusal would fail the whole batch on one // bad row. - // @external-id-checked-per-entry: src/app/api/medications/intake/bulk/route.ts + // @external-id-checked-per-entry: src/lib/medications/intake-bulk/entry.ts externalId: z.string().trim().min(1).max(128).optional(), }) .refine( @@ -181,19 +141,6 @@ const bulkPayloadSchema = z.object({ syncTrigger: z.enum(HEALTHKIT_REQUEST_SYNC_TRIGGERS).optional(), }); -// v1.8.2 — `updated` joins the per-entry status vocabulary: a write that -// snaps onto an existing scheduled-slot row (e.g. the pending REMINDER -// row) updates it in place rather than inserting. The iOS sync engine -// treats `updated` the same as `inserted` for cursor advancement — both -// mean "the server accepted this entry and produced a row id". -type EntryStatus = "inserted" | "updated" | "duplicate" | "skipped"; -interface EntryResult { - index: number; - status: EntryStatus; - reason?: string; - id?: string; -} - export const POST = apiHandler(withIdempotency<[NextRequest]>(postBulk)); async function postBulk(request: NextRequest): Promise { @@ -203,8 +150,8 @@ async function postBulk(request: NextRequest): Promise { const auth = await requireAuth(undefined, { writeBucket: "own" }); const { user } = auth; // A narrow, single-purpose credential (a bridge, an ingest token) is no - // Apple Health sync, so it never writes the dose arrival record below. The - // bare `requireAuth()` refuses such a credential today; the check keeps the + // Apple Health sync, so it never writes the dose arrival record. The bare + // `requireAuth()` refuses such a credential today; the check keeps the // record honest should this route ever declare a scope. const scoped = isScopedCredential(auth); @@ -238,7 +185,7 @@ async function postBulk(request: NextRequest): Promise { const parsed = bulkPayloadSchema.safeParse(rawBody); if (!parsed.success) { - // v1.4.43 — bulk intake ingest; preserve the existing + // Bulk intake ingest; preserve the existing // `medications.intake.bulk.invalid` errorCode meta so the iOS // Sync engine's retry classifier still branches on it. Audit // breadcrumb keyed `medications.intake.bulk.validation-failed`. @@ -247,7 +194,7 @@ async function postBulk(request: NextRequest): Promise { action: { name: "medications.intake.bulk.validation-failed" }, meta: { issue_count: issues.length }, }); - // v1.4.49 — strip `message` from the audit-ledger row. + // Strip `message` from the audit-ledger row. const auditIssues = sanitiseZodIssues(parsed.error.issues, { stripValuesFromMessage: true, }); @@ -269,776 +216,16 @@ async function postBulk(request: NextRequest): Promise { const { entries, syncTrigger } = parsed.data; - // Up-front ownership check — the user can only POST intake events - // against medications they own. One read covers every medicationId - // in the batch. - const medicationIds = Array.from(new Set(entries.map((e) => e.medicationId))); - const ownedMedications = await prisma.medication.findMany({ - where: { id: { in: medicationIds }, userId: user.id }, - // v1.8.5 — pull the injection-tracking fields so a per-entry - // `injectionSite` can be resolved + validated without an extra - // per-entry read. v1.28 — `externalSource` rides along so the - // Apple-entry mirror gate below needs no extra read either. - select: { - id: true, - deliveryForm: true, - trackInjectionSites: true, - allowedInjectionSites: true, - externalSource: true, - trackIntake: true, - }, - }); - const ownedSet = new Set(ownedMedications.map((m) => m.id)); - const medById = new Map(ownedMedications.map((m) => [m.id, m])); - - // v1.28 — source-exclusive Apple-mirrored medications: an APPLE_HEALTH - // dose event may only attach to a medication mirrored from Apple Health - // (`Medication.externalSource === "APPLE_HEALTH"`). Targeting a native - // medication is a client contract violation (the iOS sync maps HK dose - // events onto the meds it mirrored itself), not a per-entry data - // problem, so the whole batch 422s with a stable errorCode the client - // can branch on. Unowned medicationIds keep the per-entry - // `medication_not_found` skip below. - const appleMismatch = entries.some( - (e) => - e.source === "APPLE_HEALTH" && - ownedSet.has(e.medicationId) && - medById.get(e.medicationId)?.externalSource !== "APPLE_HEALTH", - ); - if (appleMismatch) { - return apiError( - "An APPLE_HEALTH entry may only target a medication mirrored from Apple Health", - 422, - { errorCode: "medications.intake.bulk.apple_health_not_mirrored" }, - ); - } - - // v1.28 — idempotent re-sync dedup: one read collects every live row - // already carrying one of the batch's external dose-event UUIDs; the - // per-entry loop reports those entries as `duplicate` (first-write-wins - // per Apple dose) without touching the DB again. Fresh inserts register - // into the same map so an intra-batch repeat of a UUID dedups too. The - // partial `(user_id, external_id)` unique (migration 0239) is the race - // backstop. - const batchExternalIds = Array.from( - new Set( - entries - .map((e) => e.externalId) - .filter((id): id is string => id !== undefined), - ), - ); - const rowIdByExternalId = new Map(); - if (batchExternalIds.length > 0) { - const existingExternalRows = await prisma.medicationIntakeEvent.findMany({ - where: { - userId: user.id, - externalId: { in: batchExternalIds }, - deletedAt: null, - }, - select: { id: true, externalId: true }, - }); - for (const row of existingExternalRows) { - if (row.externalId) rowIdByExternalId.set(row.externalId, row.id); - } - } - - // v1.8.5 — the user's global exclusion deny-list, loaded once for the - // whole batch (it is user-scoped, not per-medication). Only read when - // at least one entry actually carries an `injectionSite`, so the hot - // sync path (no site) pays for zero extra round-trips. - const batchHasInjectionSite = entries.some( - (e) => e.injectionSite !== undefined, - ); - let globalExcluded: InjectionSiteValue[] = []; - if (batchHasInjectionSite) { - const userRow = await prisma.user.findUnique({ - where: { id: user.id }, - select: { globalExcludedInjectionSites: true }, - }); - globalExcluded = (userRow?.globalExcludedInjectionSites ?? - []) as InjectionSiteValue[]; - } - - const results: EntryResult[] = []; - let inserted = 0; - let updated = 0; - let duplicates = 0; - // External dose records kept as their own unscheduled dose because their - // band slot already carried a different one. - let extraDosesUnscheduled = 0; - const skipped: Array<{ index: number; reason: string }> = []; - const unstableShapes: UnstableExternalIdShape[] = []; - // Distinct `(medicationId, dayKey)` pairs touched by the batch, so one - // dose-day refresh fires per pair after all inserts complete. A per-row - // refresh would balloon a 500-entry batch into 500 sequential refreshes. - const touchedDays = new Map< - string, - { medicationId: string; dayKey: string } - >(); - // v1.17.1 (#22) — distinct `(medicationId, scheduledFor)` slots whose - // state actually changed in this batch (a row inserted / updated), so the - // silent cross-device sync fires once per affected slot — not once per - // row, and not for pending echoes / duplicates that changed nothing. - const syncSlots = new Map< - string, - { medicationId: string; scheduledFor: string } - >(); - // A landed non-skipped dose quiets an active snooze, exactly like the - // single-intake routes (they null `snoozedUntil` on every recorded take). - // Collected per medication here, cleared once after the loop. - const snoozeClearIds = new Set(); - - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - - if (!ownedSet.has(entry.medicationId)) { - const reason = "medication_not_found"; - skipped.push({ index: i, reason }); - results.push({ index: i, status: "skipped", reason }); - continue; - } - - // A record-only medication takes no new dose (iOS #116, item 15). The - // single-dose routes answer 422 `medication.intake.notTracked`; here the - // same decision is one entry's outcome, so the rest of the batch lands. - if (isRecordOnly(medById.get(entry.medicationId) ?? {})) { - const reason = INTAKE_NOT_TRACKED_REASON; - skipped.push({ index: i, reason }); - results.push({ index: i, status: "skipped", reason }); - continue; - } - - // Both dedup keys on this route are only idempotent while the value - // is STABLE across client launches: `(userId, externalId)` for the - // Apple dose event, and the `@unique` `idempotency_key` column for - // the replay probe below. A value that rotates per process (an - // object description carrying a memory address) never matches its - // own earlier row, so every re-sync logs the dose again. Refused per - // entry — one bad row must not stop the rest of the batch landing. - const unstable = - (entry.externalId === undefined - ? null - : classifyExternalId(entry.externalId)) ?? - (entry.idempotencyKey === undefined - ? null - : classifyExternalId(entry.idempotencyKey)); - if (unstable) { - unstableShapes.push(unstable); - skipped.push({ index: i, reason: UNSTABLE_EXTERNAL_ID_REASON }); - results.push({ - index: i, - status: "skipped", - reason: UNSTABLE_EXTERNAL_ID_REASON, - }); - continue; - } - - // v1.28 — Apple dose-event replay: the batch pre-check found a live - // row already carrying this external UUID (or an earlier entry in - // THIS batch inserted it). First-write-wins per Apple dose — report - // `duplicate` with the existing row id so the sync cursor advances. - if (entry.externalId !== undefined) { - const existingRowId = rowIdByExternalId.get(entry.externalId); - if (existingRowId !== undefined) { - duplicates += 1; - results.push({ index: i, status: "duplicate", id: existingRowId }); - continue; - } - } - - try { - const incomingScheduledFor = - entry.scheduledFor ?? entry.takenAt ?? new Date(); - - // Classify the incoming write. An offline-sync replay echoes a - // PENDING projection (no `takenAt`, `skipped:false`) for a slot the - // user may already have actioned; that echo must NEVER clear a - // recorded `takenAt`. An explicit `takenAt` or `skipped:true` is a - // real user action and applies last-write-wins. - const isExplicitTaken = !entry.skipped && entry.takenAt !== undefined; - const isExplicitSkip = entry.skipped === true; - - // v1.15.20 — taken writes attribute through the window-band engine - // (the SAME minter the read ledger + the compliance % consume), so a - // bulk-synced take binds to a slot identically to the single-route - // write. The optional `forceSlotInstant` pins an off-window take onto - // a named real slot. Pending echoes and skips carry no take to - // attribute by, so they keep the canonical anchor snap that binds - // them to the projector/worker-minted slot row. - let canonicalSlot: Date | null; - // v1.15.20 — binding provenance: USER_PIN on the forced path, AUTO on - // a band decision. Pending echoes / skips carry no decision - // (undefined → the upsert leaves an existing USER_PIN untouched). - let attributionSource: "AUTO" | "USER_PIN" | undefined; - // Set when the band slot is held by a different dose and this take is - // recorded unscheduled; it must not converge onto any slot row below. - let heldByAnotherDose = false; - if (isExplicitTaken && entry.forceSlotInstant !== undefined) { - canonicalSlot = await resolveForcedSlotForWrite({ - userId: user.id, - medicationId: entry.medicationId, - userTz: user.timezone, - slotInstant: entry.forceSlotInstant, - }); - if (canonicalSlot === null) { - // Mirrors the single route's 422 (`medications.intake.force_slot - // .invalid`) as a per-entry skip so one bad pin never fails the - // batch. - const reason = "force_slot_invalid"; - skipped.push({ index: i, reason }); - results.push({ index: i, status: "skipped", reason }); - continue; - } - // v1.16.0 — refuse to pin onto a slot another recorded action - // already serves: the explicit-write last-write-wins rule would - // silently overwrite that dose record. Per-entry skip, mirroring - // the single route's 422 `medications.intake.force_slot.occupied`. - if ( - await findPinConflict({ - userId: user.id, - medicationId: entry.medicationId, - canonicalSlot, - incomingTakenAt: entry.takenAt ?? null, - }) - ) { - const reason = "force_slot_occupied"; - skipped.push({ index: i, reason }); - results.push({ index: i, status: "skipped", reason }); - continue; - } - attributionSource = "USER_PIN"; - } else if (isExplicitTaken) { - const attribution = await resolveSlotForWriteByBand({ - userId: user.id, - medicationId: entry.medicationId, - userTz: user.timezone, - takenAt: entry.takenAt as Date, - }); - canonicalSlot = attribution.slotInstant; - attributionSource = "AUTO"; - // An external dose record (a HealthKit dose event) whose band slot - // already carries a different recorded dose is an extra dose, not a - // correction of that one. Converging would overwrite the slot's - // take last-write-wins and the extra dose would exist nowhere; it is - // recorded as its own unscheduled dose instead. - if ( - canonicalSlot !== null && - entry.externalId !== undefined && - (await slotHeldByAnotherDose({ - userId: user.id, - medicationId: entry.medicationId, - canonicalSlot, - takenAt: entry.takenAt as Date, - externalId: entry.externalId, - scheduledFor: entry.scheduledFor ?? null, - userTz: user.timezone, - })) - ) { - canonicalSlot = null; - heldByAnotherDose = true; - extraDosesUnscheduled += 1; - } - } else if (isExplicitSkip && entry.scheduledFor === undefined) { - // v1.16.9 — a slot-less deliberate skip resolves by band - // membership on the skip moment, exactly like a take does. The - // defaulted-now snap below only reaches the tight reminder-grace - // window, so a skip tapped 20 minutes past the dose time inserted - // an orphan ad-hoc skip row while the slot's pending REMINDER row - // stayed open and later auto-missed — the user's deliberate skip - // recorded as a miss. - const attribution = await resolveSlotForWriteByBand({ - userId: user.id, - medicationId: entry.medicationId, - userTz: user.timezone, - takenAt: incomingScheduledFor, - }); - canonicalSlot = attribution.slotInstant; - } else { - // v1.8.2 — source-agnostic anchor snap for pending echoes + skips. - // iOS posts the reminder actions here (source API); without this - // they inserted a SECOND row for a slot already carrying the - // projector/worker's pending REMINDER row (the unique key includes - // `source`, and the iOS-vs-server `scheduledFor` drifts by a - // minute). Snap onto the canonical slot instant and update the - // existing row in place. - canonicalSlot = await resolveSlotInstantForWrite({ - userId: user.id, - medicationId: entry.medicationId, - userTz: user.timezone, - incoming: incomingScheduledFor, - // Only a client-supplied `scheduledFor` names a real slot. A - // defaulted-now write must not snap across the wide ±halfGap - // window onto a far slot (phantom morning dose). - instantIsExplicit: entry.scheduledFor !== undefined, - // Pending echoes / skips never record a take; a pending sync - // echo legitimately maps to a future slot, so the taken - // future-slot guard stays off here. - isTakenWrite: false, - }); - } - - // The `scheduledFor` instant the row actually lands on — set by the - // branch that performs the write so the rollup recompute below keys - // the same day the stored row anchors to. - let effectiveScheduledFor = canonicalSlot ?? incomingScheduledFor; - - // v1.8.5 — resolve + validate the optional per-entry injection - // site. A disallowed site marks the entry skipped without failing - // the batch (per-entry contract); a non-injection / tracking-off / - // non-taken entry silently drops it. - let resolvedInjectionSite: InjectionSiteKey | null = null; - if (entry.injectionSite !== undefined) { - const med = medById.get(entry.medicationId); - const resolution = resolveInjectionSiteForWrite({ - submitted: entry.injectionSite, - taken: isExplicitTaken, - deliveryForm: med?.deliveryForm ?? "ORAL", - trackInjectionSites: med?.trackInjectionSites ?? false, - allowedInjectionSites: (med?.allowedInjectionSites ?? - []) as InjectionSiteKey[], - globalExcludedInjectionSites: globalExcluded as InjectionSiteKey[], - }); - if (resolution.kind === "disallowed") { - const reason = "injection_site_not_allowed"; - skipped.push({ index: i, reason }); - results.push({ index: i, status: "skipped", reason }); - continue; - } - resolvedInjectionSite = resolution.site; - } - - if (canonicalSlot) { - // Scheduled dose — converge onto the one canonical slot row through - // the shared upsert: H1 deterministic selection, C2 no-downgrade - // guard, and a C1 race-safe create that re-finds + updates on a - // P2002 collision rather than misclassifying it as a duplicate and - // dropping the dose. - const applied = await applyCanonicalSlotWrite({ - client: prisma, - userId: user.id, - medicationId: entry.medicationId, - canonicalSlot, - takenAt: entry.takenAt ?? null, - skipped: entry.skipped, - isExplicitTaken, - isExplicitSkip, - idempotencyKey: entry.idempotencyKey ?? null, - // v1.28 — an Apple dose-event import stamps its provenance on a - // fresh row; every other caller keeps today's "API". - createSource: entry.source ?? "API", - injectionSite: resolvedInjectionSite, - attributionSource, - // v1.16.4 — dose override only documents a consumed dose. - doseTaken: (!entry.skipped && entry.doseTaken) || null, - // v1.28 — external dose-event UUID rides onto the landed row. - externalId: entry.externalId ?? null, - }); - if (applied.noDowngradeNoOp) { - // Pending echo onto an already-actioned slot. Report it as a - // duplicate so the iOS cursor advances WITHOUT downgrading the - // recorded dose. A pending echo is never a taken write, so it - // must never reach the inventory consume below. - duplicates += 1; - results.push({ index: i, status: "duplicate", id: applied.row.id }); - } else if (applied.outcome === "updated") { - updated += 1; - results.push({ index: i, status: "updated", id: applied.row.id }); - } else { - inserted += 1; - results.push({ index: i, status: "inserted", id: applied.row.id }); - } - // v1.16.10 — inventory: only the genuine pending→taken - // transition consumes (the upsert's `consumedTransition` flag — - // the ONLY transition that may move stock). The stamp keeps a - // replayed batch exactly-once for post-v1.16.10 rows; the - // transition gate additionally protects pre-stamp rows: a full - // journal replay re-posting historical taken doses must not - // drain today's stock through their NULL stamps. An explicit - // skip refunds a previously-taken row's stamp. Pending echoes - // carry neither flag and never touch stock. - if (applied.consumedTransition && isExplicitTaken) { - await consumeForIntake({ - client: prisma, - userId: user.id, - medicationId: entry.medicationId, - eventId: applied.row.id, - intakeAt: entry.takenAt as Date, - }); - } else if (!applied.noDowngradeNoOp && isExplicitSkip) { - await restoreForIntake({ - client: prisma, - userId: user.id, - eventId: applied.row.id, - }); - } - } else { - // Resolver-null: PRN / off-slot / future-slot-guarded. Before the - // standalone insert, two guards keep the slot from forking into a - // second live row: - // - // 1. idempotencyKey replay pre-check — a re-submission returns the - // existing row as "duplicate" (previously reached via the P2002 - // catch; the explicit probe keeps that contract now that a - // replay could otherwise converge as "updated" below). - // 2. source-agnostic convergence probe — when ANY live row already - // sits on the incoming instant (e.g. the pending REMINDER row - // the worker pre-minted on a slot the taken-write future guard - // refused to snap to), converge onto THAT row through the shared - // slot upsert instead of inserting a sibling that differs only - // by `source`. The partial unique index carries `source`, so it - // cannot catch this duplicate; the probe must. - if (entry.idempotencyKey) { - // Scoped to the caller. The key is client-supplied, so an - // unscoped read would resolve another user's row and hand its - // id back here (migration 0273 made the unique per user). - const replay = await prisma.medicationIntakeEvent.findUnique({ - where: { - userId_idempotencyKey: { - userId: user.id, - idempotencyKey: entry.idempotencyKey, - }, - }, - select: { id: true }, - }); - if (replay) { - duplicates += 1; - results.push({ index: i, status: "duplicate", id: replay.id }); - continue; - } - } - // Only an explicit client `scheduledFor` names a slot a row could - // already sit on; a defaulted anchor (takenAt / now) never does, so - // the probe is skipped on that path. - // - // Dose-safety guard: a TAKEN entry must not converge onto a slot - // whose anchor is in the future relative to the take. The resolver's - // forward guard already refused to snap a late take onto a future - // slot; the probe must honour the same invariant rather than binding - // the take forward onto that slot's pending REMINDER row (a - // late-morning dose consuming the evening slot). It records - // standalone (ad-hoc) instead. - const existingSlotRow = - !heldByAnotherDose && - entry.scheduledFor !== undefined && - mayConvergeOntoSuppliedSlot({ - skipped: entry.skipped, - takenAt: entry.takenAt ?? null, - suppliedSlot: incomingScheduledFor, - }) - ? await prisma.medicationIntakeEvent.findFirst({ - where: { - userId: user.id, - medicationId: entry.medicationId, - scheduledFor: incomingScheduledFor, - deletedAt: null, - }, - select: { id: true }, - }) - : null; - if (existingSlotRow) { - const applied = await applyCanonicalSlotWrite({ - client: prisma, - userId: user.id, - medicationId: entry.medicationId, - canonicalSlot: incomingScheduledFor, - takenAt: entry.takenAt ?? null, - skipped: entry.skipped, - isExplicitTaken, - isExplicitSkip, - idempotencyKey: entry.idempotencyKey ?? null, - // v1.28 — Apple provenance on a fresh row (see above). - createSource: entry.source ?? "API", - injectionSite: resolvedInjectionSite, - attributionSource, - doseTaken: (!entry.skipped && entry.doseTaken) || null, - // v1.28 — external dose-event UUID rides onto the landed row. - externalId: entry.externalId ?? null, - }); - if (applied.noDowngradeNoOp) { - duplicates += 1; - results.push({ index: i, status: "duplicate", id: applied.row.id }); - } else if (applied.outcome === "updated") { - updated += 1; - results.push({ index: i, status: "updated", id: applied.row.id }); - } else { - inserted += 1; - results.push({ index: i, status: "inserted", id: applied.row.id }); - } - // v1.16.10 — same inventory rule as the canonical-slot branch: - // gate on the upsert's pending→taken transition flag. - if (applied.consumedTransition && isExplicitTaken) { - await consumeForIntake({ - client: prisma, - userId: user.id, - medicationId: entry.medicationId, - eventId: applied.row.id, - intakeAt: entry.takenAt as Date, - }); - } else if (!applied.noDowngradeNoOp && isExplicitSkip) { - await restoreForIntake({ - client: prisma, - userId: user.id, - eventId: applied.row.id, - }); - } - } else { - // Genuinely standalone. Anchor the row on the intake instant — - // the documented ad-hoc contract (`scheduledFor = takenAt`) — so - // an unresolvable client anchor (a future slot instant the taken - // guard rejected) can never park a live row exactly on a slot the - // worker later mints a pending REMINDER row for. A pending echo - // (no takenAt) keeps the incoming instant. The idempotencyKey, - // when supplied, has a UNIQUE index; a racing re-submission - // returns the existing row via P2002 → status: "duplicate". - effectiveScheduledFor = entry.takenAt ?? incomingScheduledFor; - const row = await prisma.medicationIntakeEvent.create({ - data: { - userId: user.id, - medicationId: entry.medicationId, - scheduledFor: effectiveScheduledFor, - takenAt: entry.takenAt ?? null, - skipped: entry.skipped, - // v1.28 — Apple provenance on a standalone (ad-hoc) row; - // every other caller keeps today's "API". - source: entry.source ?? "API", - idempotencyKey: entry.idempotencyKey ?? null, - // v1.28 — external dose-event UUID for the idempotent - // re-sync dedup. - ...(entry.externalId !== undefined && { - externalId: entry.externalId, - }), - // v1.8.5 — site only on a resolved taken-injection entry. - ...(resolvedInjectionSite !== null && { - injectionSite: resolvedInjectionSite, - }), - // v1.16.4 — dose override only on a taken entry carrying one. - ...(!entry.skipped && - entry.doseTaken && { doseTaken: entry.doseTaken }), - }, - }); - inserted += 1; - results.push({ index: i, status: "inserted", id: row.id }); - // v1.16.10 — a fresh standalone taken row consumes inventory - // units (a fresh skip / pending row has nothing to consume or - // refund). - if (isExplicitTaken) { - await consumeForIntake({ - client: prisma, - userId: user.id, - medicationId: entry.medicationId, - eventId: row.id, - intakeAt: entry.takenAt as Date, - }); - } - } - } - const dayKey = dayKeyForScheduledFor( - effectiveScheduledFor, - user.timezone, - ); - touchedDays.set(`${entry.medicationId}|${dayKey}`, { - medicationId: entry.medicationId, - dayKey, - }); - // v1.17.1 (#22) — collect the slot for the silent cross-device sync - // only when the row's state actually changed (inserted / updated). A - // pending echo, duplicate, or no-downgrade no-op leaves the visible - // dose state untouched, so it must not wake the user's other devices. - // v1.28 — register the landed row under its external dose-event - // UUID so a repeat of the same UUID later in THIS batch dedups - // in-memory instead of tripping the partial unique. - const landed = results[results.length - 1]; - if (entry.externalId !== undefined && landed?.id !== undefined) { - rowIdByExternalId.set(entry.externalId, landed.id); - } - const lastStatus = results[results.length - 1]?.status; - if (lastStatus === "inserted" || lastStatus === "updated") { - const scheduledForIso = effectiveScheduledFor.toISOString(); - syncSlots.set(`${entry.medicationId}|${scheduledForIso}`, { - medicationId: entry.medicationId, - scheduledFor: scheduledForIso, - }); - if (!entry.skipped) snoozeClearIds.add(entry.medicationId); - } - } catch (err: unknown) { - // P2002 = unique-constraint violation. Two shapes reach here: - // 1. an idempotencyKey collision on the unscheduled/PRN insert → - // "duplicate" (the canonical-slot path already absorbs its own - // same-slot P2002 race inside `applyCanonicalSlotWrite`); - // 2. a same-slot collision on the unscheduled insert. In both - // cases we surface the existing row id so the cursor advances. - if ( - typeof err === "object" && - err !== null && - "code" in err && - (err as { code: string }).code === "P2002" - ) { - const existing = entry.idempotencyKey - ? await prisma.medicationIntakeEvent.findUnique({ - where: { - userId_idempotencyKey: { - userId: user.id, - idempotencyKey: entry.idempotencyKey, - }, - }, - select: { id: true }, - }) - : // v1.28 — an Apple entry that raced the partial - // `(user_id, external_id)` unique (concurrent batches syncing - // the same dose event) resolves to the winning live row. - entry.externalId - ? await prisma.medicationIntakeEvent.findFirst({ - where: { - userId: user.id, - externalId: entry.externalId, - deletedAt: null, - }, - select: { id: true }, - }) - : null; - duplicates += 1; - results.push({ - index: i, - status: "duplicate", - id: existing?.id, - }); - continue; - } - const reason = - err instanceof Error ? err.message.slice(0, 120) : "create_failed"; - skipped.push({ index: i, reason }); - results.push({ index: i, status: "skipped", reason }); - } - } - - await auditLog("medications.intake.bulk.ingest", { - userId: user.id, + const outcome = await ingestBulkIntake({ + user, + scoped, + entries, + syncTrigger, ipAddress: getClientIp(request), - details: { - processed: entries.length, - inserted, - updated, - duplicates, - skipped: skipped.length, - }, + originDeviceToken: request.headers.get("x-device-id"), }); - - annotate({ - action: { name: "medications.intake.bulk.ingest" }, - meta: { - processed: entries.length, - inserted, - updated, - duplicates, - skipped: skipped.length, - extra_doses_unscheduled: extraDosesUnscheduled, - ...(unstableShapes.length > 0 - ? unstableExternalIdMeta("medication.intake.bulk", unstableShapes) - : {}), - }, - }); - - // v1.4.34 — bust per-user medications + compliance + achievement - // caches when at least one row landed so the next read reflects the - // ingested batch. v1.16.8 — hard-evict: this bulk endpoint is the iOS - // user's INTERACTIVE intake path (a dose taken / skipped on the phone - // syncs through here), so the very next read must show the user their - // own action — a marked-stale SWR cell would hand back the pre-dose - // payload. The genuinely background writers (the auto-miss cron in - // `intake-auto-skip.ts`, slot dedup in `intake-slot-dedup.ts`) keep - // the default mark-stale at their own call sites. - if (inserted > 0 || updated > 0) { - invalidateUserMedications(user.id, { evict: true }); + if (outcome.kind === "refused") { + return apiError(outcome.message, 422, { errorCode: outcome.errorCode }); } - - // Refresh the derived rows of each distinct `(medicationId, dayKey)` the - // batch touched. Best-effort: the refresh reports its own failures and - // never fails the ingest. - for (const { medicationId, dayKey } of touchedDays.values()) { - await refreshDoseDay({ - userId: user.id, - medicationId, - dayKey, - tz: user.timezone, - }); - } - - // v1.17.1 (#22) — silent cross-device intake sync. ONE queued fan-out - // for the whole batch regardless of how many slots it touched (the - // per-user coalescer folds bursts further), excluding the originating - // device (`X-Device-Id` = registered `Device.token`). APNs-only, - // best-effort, fire-and-forget: the canonical rows are already - // persisted, so a sync-push miss never affects the batch response. - // The single-intake routes reset a medication's snooze on every recorded - // take; a drained offline batch must quiet the snooze the same way, or a - // dose synced in later keeps counting as snoozed although it was taken. - if (snoozeClearIds.size > 0) { - await prisma.medication.updateMany({ - where: { id: { in: [...snoozeClearIds] }, userId: user.id }, - data: { snoozedUntil: null }, - }); - } - - if (syncSlots.size > 0) { - queueMedicationIntakeSync({ - userId: user.id, - originDeviceToken: request.headers.get("x-device-id"), - }); - - // v1.18.4 — PWA-only counterpart: close the still-pending dose-due - // reminders for every affected slot over Web Push and refresh the app - // badge once with the post-batch outstanding-dose count. Best-effort, - // fire-and-forget — a clear miss never affects the batch response. - const affectedSlots = Array.from(syncSlots.values()); - void (async () => { - const badgeCount = await countOutstandingDosesToday( - user.id, - user.timezone, - ); - await dispatchMedicationIntakeWebClearBulk({ - userId: user.id, - slots: affectedSlots, - badgeCount, - }); - })(); - } - - // v1.42.4 — Apple Health doses get the per-type arrival record the - // measurement batch keeps for its types: when they last arrived, under which - // trigger, when a sync last brought a new or changed dose, and when a - // background sync last carried them. Entries sent with `source: - // "APPLE_HEALTH"` only; a dose taken in the app is not an Apple Health - // arrival. Replays answered `duplicate` alone are not one (#1259), the - // same rule as the batch route. - const doseArrival = { accepted: 0, newSamples: 0 }; - for (const result of results) { - if (entries[result.index]?.source !== "APPLE_HEALTH") continue; - if (result.status === "skipped") continue; - doseArrival.accepted += 1; - if (result.status !== "duplicate") doseArrival.newSamples += 1; - } - if (doseArrival.accepted > 0 && !scoped) { - await recordHealthKitTypeArrivals(prisma, { - userId: user.id, - counts: new Map([[MEDICATION_DOSE_ARRIVAL_TYPE, doseArrival]]), - trigger: syncTrigger ?? null, - }); - } - - // The SyncMode checkpoint belongs to the push that moved the data, not to - // the `/api/sync/state` handshake that reports it. This endpoint is one of - // the three domains that handshake summarises, so an intake drain advances - // it. A `backfill` catch-up does not: it is history, not a sync. - if (syncTrigger !== BACKFILL_SYNC_TRIGGER) { - await markSyncCheckpoint(user.id); - } - - return apiSuccess({ - processed: entries.length, - inserted, - updated, - duplicates, - skipped, - entries: results, - }); + return apiSuccess(outcome.body); } diff --git a/src/app/api/medications/intake/route.ts b/src/app/api/medications/intake/route.ts index 27604ba9d..735ac818c 100644 --- a/src/app/api/medications/intake/route.ts +++ b/src/app/api/medications/intake/route.ts @@ -22,44 +22,19 @@ import { sanitiseZodIssues, } from "@/lib/api-response"; import { annotate } from "@/lib/logging/context"; -import { prisma } from "@/lib/db"; import { auditLog } from "@/lib/auth/audit"; import { DEFAULT_TIMEZONE } from "@/lib/tz/resolver"; -import { cached, caches, type ServerCache } from "@/lib/cache/server-cache"; -import { invalidateUserMedications } from "@/lib/cache/invalidate"; -import { refreshDoseSlot } from "@/lib/medications/dose-day-refresh"; -import { - buildScheduleAnchoredComplianceBuckets, - type ScheduleAnchoredComplianceBucket, -} from "@/lib/analytics/schedule-anchored-compliance"; -import { startOfLocalDayInTz } from "@/lib/tz/local-day"; -import { projectTodayIntakesAndRecompute } from "@/lib/medications/scheduling/project-today-intakes"; -import { - applyCanonicalSlotWrite, - resolveForcedSlotForWrite, - resolveSlotForWriteByBand, -} from "@/lib/medications/scheduling/slot-upsert"; -import { resolveInjectionSiteForWrite } from "@/lib/medications/injection-site-write"; -import { - consumeForIntake, - restoreForIntake, -} from "@/lib/medications/inventory/consumption"; import { intakeAggregateQuerySchema, intakeStatusUpdateSchema, - type InjectionSiteValue, } from "@/lib/validations/medication"; -import { queueMedicationIntakeSync } from "@/lib/notifications/medication-intake-sync"; -import { dispatchMedicationIntakeWebClear } from "@/lib/notifications/web-push-clear"; -import { notifyDelegatedIntake } from "@/lib/notifications/delegated-intake"; -import { countOutstandingDosesToday } from "@/lib/medications/outstanding-doses"; -import { - TRACKED_INTAKE_EVENT_WHERE, - isRecordOnly, -} from "@/lib/medications/intake-tracking"; import { refuseUntrackedIntake } from "@/lib/medications/route-guards"; +import { readTodayIntakes } from "@/lib/medications/intake-today"; +import { readIntakeCompliance } from "@/lib/medications/intake-compliance"; +import { findIntakeForStatusUpdate } from "@/lib/medications/update-intake-status/admission"; +import { updateIntakeStatus } from "@/lib/medications/update-intake-status/update-intake-status"; -// The query + body schemas moved to `@/lib/validations/medication` when this +// The query + body schemas live in `@/lib/validations/medication` since this // endpoint joined the published contract: the OpenAPI registry generates from // the validation modules, and a schema that only exists inside a handler is // one the spec has to restate — which is a second copy to keep in step. @@ -71,22 +46,22 @@ export const GET = apiHandler(async (request: NextRequest) => { Object.fromEntries(request.nextUrl.searchParams), ); if (!parsed.success) { - // v1.4.43 — multi-issue 422 + audit breadcrumb keyed + // Multi-issue 422 + audit breadcrumb keyed // `medications.intake.list.validation-failed`. const issues = sanitiseZodIssues(parsed.error.issues); annotate({ action: { name: "medications.intake.list.validation-failed" }, meta: { issue_count: issues.length }, }); - // v1.4.49 — strip `message` from the audit-ledger row; iOS-sent - // query strings can flow into Zod issue messages. + // Strip `message` from the audit-ledger row; iOS-sent query strings can + // flow into Zod issue messages. const auditIssues = sanitiseZodIssues(parsed.error.issues, { stripValuesFromMessage: true, }); - // v1.36.0 — through `auditLog()` rather than a bare `prisma.auditLog - // .create`, because that helper is the only thing that stamps - // `actorUserId`. Filed under the resolved record either way; without the - // stamp a delegate's malformed query would read as the owner's own. + // Through `auditLog()` rather than a bare `prisma.auditLog.create`, + // because that helper is the only thing that stamps `actorUserId`. Filed + // under the resolved record either way; without the stamp a delegate's + // malformed query would read as the owner's own. void auditLog("medications.intake.list.validation-failed", { userId: user.id, details: { issues: auditIssues }, @@ -98,117 +73,24 @@ export const GET = apiHandler(async (request: NextRequest) => { const { scope, days } = parsed.data; - // v1.4.25 — anchor "today" and per-day compliance buckets to the - // user's display timezone so a 23:30 reading in Pacific/Auckland lands - // in today's bucket rather than yesterday's Berlin one. + // Anchor "today" and per-day compliance buckets to the user's display + // timezone so a 23:30 reading in Pacific/Auckland lands in today's bucket + // rather than yesterday's Berlin one. const userTz = user.timezone ?? DEFAULT_TIMEZONE; if (scope === "today") { - const todayStart = startOfLocalDayInTz(new Date(), userTz); - const todayEnd = new Date(todayStart.getTime() + 86_400_000); - - // v1.4.39 — project pending intake rows for every - // active schedule whose window opens today, then idempotently - // backfill any missing rows. Pre-fix the endpoint returned `[]` - // for daily meds (`schedule.daysOfWeek = null` in the DB) until - // the reminder worker entered the RED phase at the end of the - // dose window — leaving the iOS Dashboard tile + the "Erfassen" - // sheet empty for the whole morning. Shared helper mirrors the - // dashboard-summary call site so the two routes converge on the - // same row set. - const { projected, backfilled } = await projectTodayIntakesAndRecompute({ - userId: user.id, - userTz, - todayStart, - todayEnd, - }); - - const events = await prisma.medicationIntakeEvent.findMany({ - where: { - userId: user.id, - // v1.7.0 sync — exclude tombstoned rows from the today list. - deletedAt: null, - scheduledFor: { gte: todayStart, lt: todayEnd }, - // v1.39.1 (#1033) — the today list is what clients offer doses - // from; a medication with intake tracking off is not on it. - ...TRACKED_INTAKE_EVENT_WHERE, - }, - orderBy: { scheduledFor: "asc" }, - include: { medication: { select: { id: true, snoozedUntil: true } } }, - }); - - annotate({ - action: { name: "medications.intake.today" }, - meta: { - count: events.length, - projected, - backfilled, - }, - }); - - return apiSuccess( - events.map((e) => ({ - id: e.id, - medicationId: e.medicationId, - scheduledAt: e.scheduledFor.toISOString(), - takenAt: e.takenAt?.toISOString() ?? null, - status: e.skipped - ? "skipped" - : e.takenAt - ? "taken" - : // v1.15.9 — a never-acted dose the auto-miss cron flipped is a - // terminal MISS, not a perpetual "pending". (Today's freshly - // projected rows are < 24 h old so this rarely fires here, but - // a stale row surfacing in the window reads honestly.) - e.autoMissed - ? "missed" - : e.medication.snoozedUntil && - e.medication.snoozedUntil > new Date() - ? "snoozed" - : "pending", - snoozedUntil: e.medication.snoozedUntil?.toISOString() ?? null, - })), - ); + return apiSuccess(await readTodayIntakes({ userId: user.id, userTz })); } - // v1.4.34 — compliance: per-day scheduled vs taken for the last - // N days. Cached at a 15-minute TTL because daily compliance buckets - // are slow-moving (intake events trickle in, yesterday's row doesn't - // move). Cache key carries the userTz so a user who changes timezone - // doesn't read another tz's bucketing. - // - // v1.15.9 — `scheduled` is now the SCHEDULE-ANCHORED expected-dose count - // per day (the canonical recurrence engine), NOT the count of logged - // intake rows. The old rollup-backed path set `scheduled = COUNT(*)` of - // intake rows, so the dashboard tile's rate (`taken / scheduled`) was ~100% - // across every window regardless of real adherence — every logged row was - // both numerator and denominator. Anchoring `scheduled` to the schedule - // makes the rate genuinely reflect taken-of-expected and lets the 7/30/90 - // windows diverge with partial adherence. - const result = await cached( - caches.medicationsIntake as ServerCache, - `${user.id}|compliance|${days}|${userTz}`, - () => buildScheduleAnchoredComplianceBuckets(user.id, days, userTz), - annotate, + return apiSuccess( + await readIntakeCompliance({ userId: user.id, days, userTz }), ); - - annotate({ - action: { name: "medications.intake.compliance" }, - meta: { days, count: result.length }, - }); - - return apiSuccess(result); }); export const POST = apiHandler(async (request: NextRequest) => { - // v1.36.x — a delegated write, the canonical slot form. Everything the - // handler does downstream addresses the RECORD and stays correct: the - // cross-device intake sync wakes the OWNER's iOS devices, the web-clear - // closes the OWNER's pending dose reminder and rewrites the OWNER's badge, - // and the compliance rollup recomputes the OWNER's day. None of them can - // address the delegate, and none of them should — the delegate's feedback is - // the response they are already awaiting. What the owner gets instead is the - // notification at the end of this handler. + // A delegated write, the canonical slot form. Everything the handler does + // downstream addresses the RECORD and stays correct; what the owner gets + // is the notification at the end of the update. const { user, actor } = await requireRecordAuth("write", "medications"); const { data: body, error } = await safeJson(request, { @@ -218,18 +100,18 @@ export const POST = apiHandler(async (request: NextRequest) => { const parsed = intakeStatusUpdateSchema.safeParse(body); if (!parsed.success) { - // v1.4.43 — intake-event update hot path; multi-issue 422 + - // audit breadcrumb keyed `medications.intake.update.validation-failed`. + // Intake-event update hot path; multi-issue 422 + audit breadcrumb keyed + // `medications.intake.update.validation-failed`. const issues = sanitiseZodIssues(parsed.error.issues); annotate({ action: { name: "medications.intake.update.validation-failed" }, meta: { issue_count: issues.length }, }); - // v1.4.49 — strip `message` from the audit-ledger row. + // Strip `message` from the audit-ledger row. const auditIssues = sanitiseZodIssues(parsed.error.issues, { stripValuesFromMessage: true, }); - // v1.36.x — through `auditLog()`, the only writer that stamps the actor. + // Through `auditLog()`, the only writer that stamps the actor. void auditLog("medications.intake.update.validation-failed", { userId: user.id, details: { issues: auditIssues }, @@ -239,396 +121,29 @@ export const POST = apiHandler(async (request: NextRequest) => { return returnAllZodIssues(parsed.error, 422); } - const { - intakeId, - status, - takenAt, - snoozedUntil, - injectionSite, - forceSlotInstant, - } = parsed.data; - - // v1.7.0 sync — a tombstoned intake 404s on a status toggle; the - // `deletedAt: null` filter refuses to mutate a soft-deleted row. - const existing = await prisma.medicationIntakeEvent.findFirst({ - where: { id: intakeId, deletedAt: null }, - include: { - medication: { - select: { - deliveryForm: true, - trackInjectionSites: true, - allowedInjectionSites: true, - trackIntake: true, - }, - }, - }, + const existing = await findIntakeForStatusUpdate({ + userId: user.id, + intakeId: parsed.data.intakeId, }); - if (!existing || existing.userId !== user.id) { + if (!existing) { return apiError("Intake event not found", 404); } - // A record-only medication keeps the doses it already has, and an owner may - // still correct one of those (that is an edit of history). What it no longer - // takes is a NEW dose: resolving or snoozing an event that is still open - // would record one, and it would then be counted nowhere (iOS #116, item - // 15). Refused by name, the same code the per-medication route answers with. - const stillOpen = existing.takenAt === null && !existing.skipped; - if (stillOpen && isRecordOnly(existing.medication)) { - return refuseUntrackedIntake(); - } - - // v1.36.1 — this route is admitted for delegates because marking a dose is - // the thing a person standing next to the patient needs to do. It is an - // UPDATE, though, and two of the transitions it can express are not that. - // - // Snoozing writes `snoozedUntil` on the OWNER's medication row, and the - // reminder cron skips a medication for as long as that stamp is in the - // future. The field is unbounded, the owner is deliberately not notified of - // a delegated snooze (there is nothing useful to say about a thirty-minute - // deferral), and the activity feed renders the whole family as "marked a - // dose". So a delegate could switch the owner's medication reminders off for - // years, and every surface that exists to make delegation visible would - // describe it as marking a dose. In an adherence product that is the worst - // thing on this route. - // - // Flipping an already-resolved event is the other one: taken to skipped and - // back rewrites the owner's compliance history and refunds inventory. The - // shipped promise, on the consent screen and in the release notes, is that - // editing what is already there stays with the owner. Marking a dose that is - // still open is a contribution; changing a decision the owner already - // recorded is an edit. - // - // Both refuse for a delegate and stay open to the owner, who is the only - // person the snooze and the correction were ever for. - if (actor.id !== user.id) { - const alreadyResolved = existing.takenAt !== null || existing.skipped; - const refusal = - status === "snoozed" - ? "snooze" - : alreadyResolved - ? "resolved_event" - : null; - if (refusal) { - annotate({ - action: { name: "medications.intake.update.refused" }, - meta: { reason: refusal }, - }); - // Filed under the OWNER. `auditLog` stamps the actor itself from the - // request's acting context, which is what puts this on the trail the - // owner's activity panel reads — a refused attempt to change their - // record is worth as much to them as a successful one. - await auditLog("medications.intake.update.refused", { - userId: user.id, - ipAddress: getClientIp(request), - details: { intakeId, status, reason: refusal }, - }); - return apiError( - refusal === "snooze" - ? "Snoozing a reminder is not part of shared access" - : "Changing a dose that is already recorded is not part of shared access", - 403, - { errorCode: "sharing.not_permitted" }, - ); - } - } - - // v1.8.5 — resolve + server-validate the optional injection site for a - // "taken" toggle. A site outside the medication's effective allowed - // set is a hard 422; non-injection / tracking-off / non-taken drops it. - let resolvedInjectionSite: InjectionSiteValue | null = null; - if (injectionSite !== undefined) { - const userRow = await prisma.user.findUnique({ - where: { id: user.id }, - select: { globalExcludedInjectionSites: true }, - }); - const resolution = resolveInjectionSiteForWrite({ - submitted: injectionSite, - taken: status === "taken", - deliveryForm: existing.medication.deliveryForm, - trackInjectionSites: existing.medication.trackInjectionSites, - allowedInjectionSites: existing.medication - .allowedInjectionSites as InjectionSiteValue[], - globalExcludedInjectionSites: (userRow?.globalExcludedInjectionSites ?? - []) as InjectionSiteValue[], - }); - if (resolution.kind === "disallowed") { - annotate({ - action: { name: "medication.intake.injection_site.disallowed" }, - meta: { medication_id: existing.medicationId, site: resolution.site }, - }); - return apiError( - "Injection site is not allowed for this medication", - 422, - { - errorCode: "medications.intake.injection_site.disallowed", - }, - ); - } - resolvedInjectionSite = resolution.site; - } - - const userTzForHook = user.timezone ?? DEFAULT_TIMEZONE; - - let updated; - if (status === "taken") { - const resolvedTakenAt = takenAt ?? new Date(); - - // v1.15.18 — re-run window-band attribution on a taken toggle so an - // edited / off-window take re-binds to the right slot instead of leaving - // `scheduledFor` stale (audit HIGH-4). `forceSlotInstant` pins onto a - // named real slot (422 if it is not one); otherwise band membership picks - // the slot (the take's own time on a miss → ad-hoc). Mirrors the - // per-event PUT route. - let targetScheduledFor: Date; - if (forceSlotInstant !== undefined) { - const forced = await resolveForcedSlotForWrite({ - userId: user.id, - medicationId: existing.medicationId, - userTz: userTzForHook, - slotInstant: forceSlotInstant, - }); - if (forced === null) { - annotate({ - action: { name: "medication.intake.force_slot.invalid" }, - meta: { medication_id: existing.medicationId, intake_id: intakeId }, - }); - return apiError( - "forceSlotInstant is not a scheduled slot of this medication", - 422, - { errorCode: "medications.intake.force_slot.invalid" }, - ); - } - targetScheduledFor = forced; - } else { - const attribution = await resolveSlotForWriteByBand({ - userId: user.id, - medicationId: existing.medicationId, - userTz: userTzForHook, - takenAt: resolvedTakenAt, - }); - targetScheduledFor = attribution.slotInstant ?? resolvedTakenAt; - } - - const slotMoved = - targetScheduledFor.getTime() !== existing.scheduledFor.getTime(); - - // v1.16.10 — transition gate for the inventory consume below. Only - // a row moving INTO taken may consume; a re-take of an already- - // taken row (e.g. a time correction) leaves its stamp frozen, and a - // pre-v1.16.10 taken row (NULL stamp, stock already moved by the - // legacy hook at take time) must never retro-consume. - const wasTaken = existing.takenAt !== null && !existing.skipped; - if (!slotMoved) { - [updated] = await prisma.$transaction([ - prisma.medicationIntakeEvent.update({ - where: { id: intakeId }, - // v1.7.0 sync — bump the reconciliation counter on every - // server-side mutation so the delta feed echoes a monotonic value. - data: { - takenAt: resolvedTakenAt, - skipped: false, - // v1.42.2 — the person confirmed it; no longer automatic. - autoRecorded: false, - syncVersion: { increment: 1 }, - // v1.8.5 — persist the resolved site on the taken branch. - ...(resolvedInjectionSite !== null && { - injectionSite: resolvedInjectionSite, - }), - }, - }), - prisma.medication.update({ - where: { id: existing.medicationId }, - data: { snoozedUntil: null }, - }), - ]); - // v1.16.10 — the toggle recorded a take; consume inventory units - // on the genuine pending→taken (or skipped→taken) transition. The - // stamp additionally keeps a racing replay exactly-once. - if (!wasTaken) { - await consumeForIntake({ - client: prisma, - userId: user.id, - medicationId: existing.medicationId, - eventId: intakeId, - intakeAt: resolvedTakenAt, - }); - } - } else { - // The take re-attributed to a different slot. Tombstone the source row - // and route the dose through the shared canonical-slot upsert, which - // converges onto any row already at the target slot rather than - // bare-updating into an occupied slot (P2002-safe). - // - // v1.16.10 — refund the source row's consumption stamp BEFORE the - // tombstone, then consume on the converged row below: a slot move - // is a re-binding of one real dose, so the stock nets exactly one - // consumption. - await restoreForIntake({ - client: prisma, - userId: user.id, - eventId: intakeId, - }); - await prisma.medicationIntakeEvent.update({ - where: { id: intakeId }, - data: { deletedAt: new Date(), syncVersion: { increment: 1 } }, - }); - const applied = await applyCanonicalSlotWrite({ - client: prisma, - userId: user.id, - medicationId: existing.medicationId, - canonicalSlot: targetScheduledFor, - takenAt: resolvedTakenAt, - skipped: false, - isExplicitTaken: true, - isExplicitSkip: false, - idempotencyKey: null, - createSource: "WEB", - // v1.16.9 — carry the recorded site / dose override across the - // tombstone + re-insert: a slot move is a re-binding, not a new - // dose, so the original row's documentation must survive when the - // toggle write itself carries none. - injectionSite: - resolvedInjectionSite ?? - (existing.injectionSite as InjectionSiteValue | null), - doseTaken: existing.doseTaken, - }); - updated = applied.row; - await prisma.medication.update({ - where: { id: existing.medicationId }, - data: { snoozedUntil: null }, - }); - // v1.16.10 — consume on the converged row (the old row's stamp - // was refunded above, so the move nets one consumption). A taken - // source WITHOUT a stamp is a pre-v1.16.10 row whose stock moved - // through the legacy hook: the refund above was a no-op, so - // consuming on the target would double-charge — skip it. - if (!wasTaken || existing.inventoryConsumption !== null) { - await consumeForIntake({ - client: prisma, - userId: user.id, - medicationId: existing.medicationId, - eventId: applied.row.id, - intakeAt: resolvedTakenAt, - }); - } - } - } else if (status === "skipped") { - updated = await prisma.medicationIntakeEvent.update({ - where: { id: intakeId }, - // v1.7.0 sync — bump the reconciliation counter on the skip toggle. - // v1.42.2 — a skip the person records clears the automatic marker. - data: { - takenAt: null, - skipped: true, - autoRecorded: false, - syncVersion: { increment: 1 }, - }, - }); - // v1.16.10 — the row stopped being taken; refund whatever its - // consumption stamp recorded (no-op for a never-consumed row). - await restoreForIntake({ - client: prisma, - userId: user.id, - eventId: intakeId, - }); - } else { - // snoozed: snoozedUntil lives on the Medication row. - const until = snoozedUntil ?? new Date(Date.now() + 30 * 60_000); // default +30min - await prisma.medication.update({ - where: { id: existing.medicationId }, - data: { snoozedUntil: until }, - }); - updated = await prisma.medicationIntakeEvent.findUnique({ - where: { id: intakeId }, + const outcome = await updateIntakeStatus( + { + user, + actorId: actor.id, + existing, + ipAddress: getClientIp(request), + originDeviceToken: request.headers.get("x-device-id"), + }, + parsed.data, + ); + if (outcome.kind === "untracked") return refuseUntrackedIntake(); + if (outcome.kind === "refused") { + return apiError(outcome.message, outcome.status, { + errorCode: outcome.errorCode, }); } - - await auditLog("medications.intake.update", { - userId: user.id, - ipAddress: getClientIp(request), - details: { intakeId, status }, - }); - - annotate({ - action: { name: "medications.intake.update" }, - meta: { intakeId, status }, - }); - - // v1.4.34 — bust the medications + compliance + achievement - // caches for this user so the next read reflects the dose change. - invalidateUserMedications(user.id, { evict: true }); - - // v1.4.39 — refresh the persistent compliance rollup row for - // the affected day so the next read after the cache miss returns - // the up-to-date `(scheduled, taken, skipped)` tuple. Best-effort: - // a populator failure annotates ops without blocking the response. - await refreshDoseSlot({ - userId: user.id, - medicationId: existing.medicationId, - scheduledFor: existing.scheduledFor, - tz: userTzForHook, - }); - // v1.16.9 — a slot move re-binds the dose onto a DIFFERENT instant, - // possibly a different local day. Recompute the target day too, or the - // source day self-corrects while the destination keeps its stale tuple. - const movedTo = updated?.scheduledFor; - if (movedTo && movedTo.getTime() !== existing.scheduledFor.getTime()) { - await refreshDoseSlot({ - userId: user.id, - medicationId: existing.medicationId, - scheduledFor: movedTo, - tz: userTzForHook, - }); - } - - // v1.17.1 (#22) — silent cross-device intake sync. Wake the user's OTHER - // iOS devices so a running Live Activity / Home-Screen widget reconciles - // the dose state changed here. APNs-only, best-effort, coalesced per user: - // the canonical row is already persisted, so a sync-push miss never - // affects the response. The originating device (the one that POSTed) is - // excluded via its `X-Device-Id` (= registered `Device.token`). - queueMedicationIntakeSync({ - userId: user.id, - originDeviceToken: request.headers.get("x-device-id"), - }); - - // v1.18.4 — PWA-only equivalent of the Live Activity end: when a dose is - // resolved (taken / skipped), push a `type:"clear"` to the user's Web Push - // subscriptions so the still-pending dose-due reminder for this slot is - // closed by the service worker (matched on the stable slot tag) and the app - // badge re-reflects the outstanding-dose count. A snooze leaves the dose - // outstanding, so it neither clears the reminder nor changes the badge. - if (status === "taken" || status === "skipped") { - void (async () => { - const badgeCount = await countOutstandingDosesToday( - user.id, - userTzForHook, - ); - await dispatchMedicationIntakeWebClear({ - userId: user.id, - medicationId: existing.medicationId, - scheduledFor: (movedTo ?? existing.scheduledFor).toISOString(), - badgeCount, - }); - })(); - } - - // v1.36.x — "somebody else marked your dose". The helper refuses on self and - // on a snooze, resolves both names itself, and never throws. - // - // Fire and forget, like every neighbour above. The dispatcher awaits each - // channel in the cascade, so an unreachable one would have added its whole - // timeout to the caregiver's request — the person standing next to the - // patient waiting for a dose to register. The helper never throws, and the - // owner learning a moment later is the right trade against the tick being - // slow. The integration test already polls the ledger rather than relying on - // the await, so nothing about the proof weakens. - void notifyDelegatedIntake({ - ownerId: user.id, - actorId: actor.id, - medicationId: existing.medicationId, - status, - }); - - return apiSuccess(updated); + return apiSuccess(outcome.row); }); diff --git a/src/app/api/mood-entries/bulk/route.ts b/src/app/api/mood-entries/bulk/route.ts index b0564355e..407b3015e 100644 --- a/src/app/api/mood-entries/bulk/route.ts +++ b/src/app/api/mood-entries/bulk/route.ts @@ -20,6 +20,9 @@ * Locked contract — see `.planning/v15-ios-handoff/06-ios-responsibilities.md` * §"Cumulative metrics" sibling section for the SyncMode rationale, * and `08-locked-contracts.md` §2 for the batch envelope shape. + * + * The route authenticates, meters, caps and parses; the per-entry writes live + * in `src/lib/mood/bulk-ingest.ts`. */ import { NextRequest } from "next/server"; import { z } from "zod/v4"; @@ -27,7 +30,6 @@ import { z } from "zod/v4"; import { prisma } from "@/lib/db"; import { apiHandler, requireAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; -import { auditLog } from "@/lib/auth/audit"; import { apiError, apiSuccess, @@ -38,31 +40,9 @@ import { } from "@/lib/api-response"; import { withIdempotency } from "@/lib/idempotency"; import { checkRateLimit } from "@/lib/rate-limit"; -import { markSyncCheckpoint } from "@/lib/sync/checkpoint"; -import { - getScoreForMood, - moodContextSchema, - moodLevelEnum, - moodSourceEnum, -} from "@/lib/validations/mood"; -import { - classifyExternalId, - unstableExternalIdMeta, - UNSTABLE_EXTERNAL_ID_REASON, - type UnstableExternalIdShape, -} from "@/lib/validations/external-id"; -import { moodDateKey } from "@/lib/mood/date-key"; +import { moodLevelEnum, moodSourceEnum } from "@/lib/validations/mood"; import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; -import { levelAForWrite } from "@/lib/mood/level-a"; -import { persistMoodContext } from "@/lib/mood/context"; -import { encryptNote } from "@/lib/crypto/note-cipher"; -import { invalidateUserMood } from "@/lib/cache/invalidate"; -import { recomputeMoodBucketsForEntry } from "@/lib/rollups/mood-rollups"; -import { - createTagLinks, - droppedLinkKeysForWire, - NO_DROPPED_LINK_KEYS, -} from "@/lib/mood/tag-links"; +import { ingestMoodBulk } from "@/lib/mood/bulk-ingest"; import { isoDateTime } from "@/lib/validations/datetime"; const MAX_ENTRIES_PER_BATCH = 500; @@ -110,12 +90,12 @@ const bulkEntrySchema = z.object({ * to stay correct. * * `unknown` here and parsed with {@link bulkDimensionsSchema} PER ENTRY in - * the loop below, for the same reason `context` is: the doc comment beside + * the bulk ingest, for the same reason `context` is: the doc comment beside * this field used to promise that an out-of-range value marks THIS entry * skipped while the field itself sat in the batch schema and 422'd all five * hundred. The words were right and the code was not. A phone draining a * year of local history must not lose the year to one row. - * @per-entry-parse: bulkDimensionsSchema, in the loop below + * @per-entry-parse: bulkDimensionsSchema, in src/lib/mood/bulk-ingest.ts */ a1: z.unknown().optional(), a2: z.unknown().optional(), @@ -128,12 +108,12 @@ const bulkEntrySchema = z.object({ * context surface must not blank one filled in on the web. * * Declared as `unknown` here and parsed with `moodContextSchema` PER ENTRY - * in the loop below, deliberately. Validating it in the batch schema would + * in the bulk ingest, deliberately. Validating it in the batch schema would * make one entry carrying a retired key fail all five hundred, and this * endpoint's whole contract — stated in its own OpenAPI description — is * that a bad row is `skipped` and the rest of the batch lands. A phone * draining a year of local history must not lose the year to one row. - * @per-entry-parse: moodContextSchema, in the loop below + * @per-entry-parse: moodContextSchema, in src/lib/mood/bulk-ingest.ts */ context: z.unknown().optional(), moodLoggedAt: isoDateTime().transform((s) => new Date(s)), @@ -146,9 +126,9 @@ const bulkEntrySchema = z.object({ * existing `(userId, date, moodLoggedAt)` unique index still * protects against straight-up duplicates. * - * Stability is enforced per entry in the loop below, not at the field: + * Stability is enforced per entry in the bulk ingest, not at the field: * a schema-level refusal would fail the whole batch on one bad row. - * @external-id-checked-per-entry: src/app/api/mood-entries/bulk/route.ts + * @external-id-checked-per-entry: src/lib/mood/bulk-ingest.ts */ externalId: z.string().min(1).max(120).optional(), }); @@ -174,23 +154,6 @@ const bulkPayloadSchema = z.object({ entries: z.array(bulkEntrySchema).min(1).max(MAX_ENTRIES_PER_BATCH), }); -type EntryStatus = "inserted" | "duplicate" | "skipped"; -interface EntryResult { - index: number; - status: EntryStatus; - reason?: string; - id?: string; - // v1.12.1 — echo the client-supplied source-stable id back on each - // result so iOS can map a server row id onto its local SwiftData row - // without re-deriving it. Omitted when the entry sent no externalId. - externalId?: string; - // Submitted keys the catalog did not resolve (unknown, archived, another - // account's custom tag, or the wrong kind). Present only when non-empty, - // on an "inserted" or "duplicate" result: the entry landed, these did not. - droppedTagKeys?: string[]; - droppedFactorKeys?: string[]; -} - export const POST = apiHandler(withIdempotency<[NextRequest]>(postBulk)); async function postBulk(request: NextRequest): Promise { @@ -259,416 +222,14 @@ async function postBulk(request: NextRequest): Promise { }); } - const { entries } = parsed.data; - const tz = user.timezone ?? DEFAULT_TIMEZONE; - - const results: EntryResult[] = []; - let inserted = 0; - let duplicates = 0; - let droppedTagKeyCount = 0; - let droppedFactorKeyCount = 0; - const skipped: Array<{ index: number; reason: string }> = []; - const unstableShapes: UnstableExternalIdShape[] = []; - - // Perf — a single indexed existence read replaces the per-entry - // `findUnique` probe (up to 500 sequential round trips pre-fix). Every - // entry still resolves through exactly the key its own dedup contract - // uses (externalId when carried, else the legacy date+moodLoggedAt key), - // so lookups below are byte-identical to the per-entry probe — only the - // read is now batched. The per-entry `upsert` + tombstone check + tag - // links + reverse push all stay unchanged; batching those further would - // risk the correctness-sensitive per-row side effects for a write path - // that is usually small (a normal sync posts the current + previous - // local day, not a full historical backfill). - interface ExistingMoodRow { - id: string; - deletedAt: Date | null; - } - const orClauses: Array< - | { source: string; externalId: string } - | { date: string; moodLoggedAt: Date } - > = entries.map((entry) => - entry.externalId - ? { source: entry.source, externalId: entry.externalId } - : { - date: moodDateKey(entry.moodLoggedAt, tz), - moodLoggedAt: entry.moodLoggedAt, - }, + return apiSuccess( + await ingestMoodBulk({ + userId: user.id, + tz: user.timezone ?? DEFAULT_TIMEZONE, + entries: parsed.data.entries, + ipAddress: getClientIp(request), + parseDimensions: (dimensions) => + bulkDimensionsSchema.safeParse(dimensions), + }), ); - const existingRows = - orClauses.length > 0 - ? await prisma.moodEntry.findMany({ - where: { userId: user.id, OR: orClauses }, - select: { - id: true, - deletedAt: true, - source: true, - externalId: true, - date: true, - moodLoggedAt: true, - }, - }) - : []; - const existingByExternalKey = new Map(); - const existingByDateKey = new Map(); - for (const row of existingRows) { - if (row.externalId !== null) { - existingByExternalKey.set(`${row.source}::${row.externalId}`, row); - } - existingByDateKey.set( - `${row.date}::${row.moodLoggedAt.toISOString()}`, - row, - ); - } - - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - - // The `(userId, source, externalId)` dedup key only holds while the - // id is STABLE across client launches; a per-process value (an object - // description carrying a memory address) mints a fresh entry on every - // re-post. Per-entry refusal, never a whole-batch 422. - const unstable = - entry.externalId === undefined - ? null - : classifyExternalId(entry.externalId); - if (unstable) { - unstableShapes.push(unstable); - skipped.push({ index: i, reason: UNSTABLE_EXTERNAL_ID_REASON }); - results.push({ - index: i, - status: "skipped", - reason: UNSTABLE_EXTERNAL_ID_REASON, - externalId: entry.externalId, - }); - continue; - } - - // The context, parsed for THIS entry. A value outside the vocabulary - // skips this row and nothing else. - const parsedContext = - entry.context === undefined - ? { success: true as const, data: undefined } - : moodContextSchema.nullable().safeParse(entry.context); - if (!parsedContext.success) { - const reason = "invalid_context"; - skipped.push({ index: i, reason }); - results.push({ - index: i, - status: "skipped", - reason, - ...(entry.externalId ? { externalId: entry.externalId } : {}), - }); - continue; - } - - // The five dimensions, parsed for THIS entry. Out of range skips this row - // and nothing else. - const parsedDimensions = bulkDimensionsSchema.safeParse({ - a1: entry.a1, - a2: entry.a2, - a3: entry.a3, - a4: entry.a4, - a5: entry.a5, - }); - if (!parsedDimensions.success) { - const reason = "invalid_dimensions"; - skipped.push({ index: i, reason }); - results.push({ - index: i, - status: "skipped", - reason, - ...(entry.externalId ? { externalId: entry.externalId } : {}), - }); - continue; - } - - const date = moodDateKey(entry.moodLoggedAt, tz); - const score = getScoreForMood(entry.mood); - // v1.37 / v1.38 — the same resolution the single-entry path uses: - // pleasantness derived from the label unless the entry states its own, and - // the other four present only when this entry carried them. An omitted - // dimension is left alone rather than written as null, so a row that - // already carries hand-set values keeps them across a re-import. - const levelA = levelAForWrite(entry.mood, parsedDimensions.data); - - try { - // v1.12.1 — when the entry carries a source-stable `externalId`, - // dedup on the NULL-distinct `(userId, source, externalId)` key so a - // re-post with the same id (an iOS retry after a network hiccup, or a - // second adopt-on-pair backfill) updates the existing row in place - // instead of minting a duplicate when `moodLoggedAt` re-rounds / - // re-zones. Absent → the legacy `(userId, date, moodLoggedAt)` key. - // `source` carries a schema default of "MANUAL"; resolve it once so - // the probe, upsert key, and create write all agree on the value. - const resolvedSource = entry.source; - const probeWhere = entry.externalId - ? { - userId_source_externalId: { - userId: user.id, - source: resolvedSource, - externalId: entry.externalId, - }, - } - : { - userId_date_moodLoggedAt: { - userId: user.id, - date, - moodLoggedAt: entry.moodLoggedAt, - }, - }; - - // The response reliably distinguishes "inserted" from "duplicate" - // via the batched pre-read above (keyed exactly like `probeWhere`), - // so the per-entry probe here is now a Map lookup, not a round trip. - const existing = - (entry.externalId - ? existingByExternalKey.get(`${resolvedSource}::${entry.externalId}`) - : existingByDateKey.get( - `${date}::${entry.moodLoggedAt.toISOString()}`, - )) ?? null; - - // Tombstone suppression: a soft-deleted match stays deleted. The - // user-facing DELETE route flips `deletedAt` so the `/api/sync/changes` - // feed surfaces the deletion to paired clients offline at delete time - // (see the MoodEntry schema note) — an offline client's later re-post - // of the same entry is stale state, not a recreation, and must not - // resurrect the row. True no-op: no value churn, no `updatedAt` bump, - // no tag-link writes on the hidden row; report `duplicate` so the - // client checkpoints past it exactly like a live-row match. - if (existing?.deletedAt) { - duplicates += 1; - results.push({ - index: i, - status: "duplicate", - id: existing.id, - ...(entry.externalId ? { externalId: entry.externalId } : {}), - }); - continue; - } - - // The upsert and the tag/factor links run in ONE transaction, matching - // the single-entry POST path. Split apart, an out-of-scale - // `ratedFactors` value (validated inside `createTagLinks`, i.e. AFTER - // the row committed) left a persisted mood entry with no links while the - // per-entry catch reported the entry as "skipped" — the client believes - // nothing saved and a half-written row is on disk. - const written = await prisma.$transaction(async (tx) => { - const upserted = await tx.moodEntry.upsert({ - where: probeWhere, - create: { - userId: user.id, - date, - tz, - mood: entry.mood, - score, - ...levelA, - tags: entry.tags ? JSON.stringify(entry.tags) : null, - note: null, - noteEncrypted: encryptNote(entry.note ?? null), - source: resolvedSource, - externalId: entry.externalId ?? null, - moodLoggedAt: entry.moodLoggedAt, - }, - update: { - // Last-writer-wins on the mood + tags + note triple. The - // iOS client only re-posts an existing entry when it has - // new data; the server trusts that decision. When the dedup - // key is `externalId`, also refresh `date` / `moodLoggedAt` - // so a re-zoned re-import lands the corrected wall-clock on - // the same row. - mood: entry.mood, - score, - ...levelA, - tags: entry.tags ? JSON.stringify(entry.tags) : null, - note: null, - noteEncrypted: encryptNote(entry.note ?? null), - ...(entry.externalId - ? { date, moodLoggedAt: entry.moodLoggedAt } - : {}), - }, - }); - - // v1.12.0 — persist structured-tag links, mirroring the - // single-entry `createTagLinks` path. Additive + idempotent: - // `createTagLinks` resolves keys against the catalog (dropping - // unknown keys) and `skipDuplicates` on the join insert keeps a - // re-posted entry from minting duplicate links. Runs for both - // fresh and re-posted (upserted) rows so a backfill that adds tag - // keys on a second pass still lands them. - // v1.12.0 — rated factors ride the same path; an out-of-scale - // rating throws `RatedFactorOutOfRangeError`. Inside the - // transaction that throw now rolls the mood row back with it, so a - // "skipped" result means nothing was written — which is what the - // client was already being told. - // v1.38 — the day context, inside the same transaction as the row, so - // an entry reported as skipped really did write nothing. - await persistMoodContext(tx, upserted.id, user.id, parsedContext.data); - - // The keys the catalog did not resolve are reported on this entry's - // result: "inserted" alone told a client its tags had landed. - let droppedKeys = NO_DROPPED_LINK_KEYS; - if ( - (entry.tagKeys && entry.tagKeys.length > 0) || - (entry.ratedFactors && entry.ratedFactors.length > 0) - ) { - droppedKeys = await createTagLinks( - upserted.id, - user.id, - entry.tagKeys ?? [], - tx, - entry.ratedFactors ?? [], - ); - } - - return { upserted, droppedKeys }; - }); - const result = written.upserted; - const droppedKeys = written.droppedKeys; - droppedTagKeyCount += droppedKeys.droppedTagKeys.length; - droppedFactorKeyCount += droppedKeys.droppedFactorKeys.length; - - if (existing) { - duplicates += 1; - results.push({ - index: i, - status: "duplicate", - id: result.id, - ...(entry.externalId ? { externalId: entry.externalId } : {}), - ...droppedLinkKeysForWire(droppedKeys), - }); - } else { - inserted += 1; - results.push({ - index: i, - status: "inserted", - id: result.id, - ...(entry.externalId ? { externalId: entry.externalId } : {}), - ...droppedLinkKeysForWire(droppedKeys), - }); - } - } catch (err: unknown) { - // v1.37.19 — P2002 = a concurrent batch raced the same - // NULL-distinct `(userId, source, externalId)` key and won. That is - // the plain duplicate the sequential probe would have classified; - // reporting it as "skipped" with a raw Prisma message told the - // client its row was dropped when it is in fact stored. Resolve the - // winning row's id so the caller's cursor advances, exactly like - // the intake bulk route. - if ( - typeof err === "object" && - err !== null && - "code" in err && - (err as { code: string }).code === "P2002" && - entry.externalId - ) { - const winner = await prisma.moodEntry.findFirst({ - where: { - userId: user.id, - // Same resolution as the upsert key: the schema defaults - // `source` to "MANUAL" at parse time. - source: entry.source, - externalId: entry.externalId, - }, - select: { id: true }, - }); - duplicates += 1; - results.push({ - index: i, - status: "duplicate", - ...(winner ? { id: winner.id } : {}), - externalId: entry.externalId, - }); - continue; - } - const reason = - err instanceof Error ? err.message.slice(0, 120) : "upsert_failed"; - skipped.push({ index: i, reason }); - results.push({ - index: i, - status: "skipped", - reason, - ...(entry.externalId ? { externalId: entry.externalId } : {}), - }); - } - } - - await auditLog("mood.bulk.ingest", { - userId: user.id, - ipAddress: getClientIp(request), - details: { - processed: entries.length, - inserted, - duplicates, - skipped: skipped.length, - }, - }); - - annotate({ - action: { name: "mood.bulk.ingest" }, - meta: { - processed: entries.length, - inserted, - duplicates, - skipped: skipped.length, - dropped_tag_keys: droppedTagKeyCount, - dropped_factor_keys: droppedFactorKeyCount, - ...(unstableShapes.length > 0 - ? unstableExternalIdMeta("mood.bulk", unstableShapes) - : {}), - }, - }); - - // v1.4.34 — bust per-user mood + achievements + analytics caches - // when at least one row landed so the next read picks up the ingested - // batch. Skipped / duplicate-only ingests are no-ops. - if (inserted > 0) { - invalidateUserMood(user.id); - } - - // v1.4.39 — refresh the rollup tier for every distinct day - // touched by this batch. The bulk endpoint is an iOS one-shot - // backfill so the batch can span many days; we collapse to the - // unique `(user, dayStart)` set first to bound the recompute count. - // Best-effort: rollup failures must not surface as 5xx. - if (inserted > 0 || duplicates > 0) { - // v1.32.12 — collapse to the unique set of `date` labels touched by - // this batch (the same per-row `moodDateKey` the insert used), so - // the rollup keys byte-identically to the stored `MoodEntry.date`. - const touchedLabels = new Set(); - for (let i = 0; i < entries.length; i++) { - const status = results[i]?.status; - if (status === "inserted" || status === "duplicate") { - touchedLabels.add(moodDateKey(entries[i].moodLoggedAt, tz)); - } - } - try { - await Promise.all( - Array.from(touchedLabels).map((label) => - recomputeMoodBucketsForEntry(user.id, label), - ), - ); - } catch (rollupErr) { - annotate({ - meta: { - mood_rollup_write_failed: true, - mood_rollup_write_error: - rollupErr instanceof Error ? rollupErr.message : String(rollupErr), - }, - }); - } - } - - // The SyncMode checkpoint belongs to the push that moved the data, not to - // the `/api/sync/state` handshake that reports it. This endpoint is one of - // the three domains that handshake summarises, so a mood drain advances it. - await markSyncCheckpoint(user.id); - - return apiSuccess({ - processed: entries.length, - inserted, - duplicates, - skipped, - entries: results, - }); } diff --git a/src/app/api/sync/changes/route.ts b/src/app/api/sync/changes/route.ts index 28d268df6..d714e7fff 100644 --- a/src/app/api/sync/changes/route.ts +++ b/src/app/api/sync/changes/route.ts @@ -46,16 +46,11 @@ * `apiHandler` + `requireAuth` (cookie OR Bearer; iOS uses Bearer). * Read-only — no idempotency, no write side-effect (unlike the legacy * `/api/sync/state` checkpoint bump). The cursor is owned by the client. + * The pull itself lives in `src/lib/sync/changes/`. */ import type { NextRequest } from "next/server"; import { z } from "zod/v4"; -import { prisma } from "@/lib/db"; -import { - findCompactionTombstones, - isFoldedWindowCandidate, - loadFoldTimezone, -} from "@/lib/measurements/folded-window"; import { apiHandler, requireAuth } from "@/lib/api-handler"; import { apiError, @@ -63,153 +58,17 @@ import { apiValidationError, sanitiseZodIssues, } from "@/lib/api-response"; -import { annotate } from "@/lib/logging/context"; import { checkRateLimit } from "@/lib/rate-limit"; -import { readNote } from "@/lib/crypto/note-cipher"; -import { levelAForWire } from "@/lib/mood/level-a"; -import { contextForWire, type MoodContextWire } from "@/lib/mood/context"; -import { TOMBSTONE_RETENTION_DAYS } from "@/lib/auth/native-client"; -import { - decodeCursor, - decodeCursorIssuedAt, - encodeCursor, - SYNC_DOMAINS, - type DomainWatermark, - type SyncCursor, - type SyncDomain, -} from "@/lib/sync/cursor"; -import { - toCycleDayLogDTO, - toMenstrualCycleDTO, - dayLogSymptomInclude, - type CycleDayLogDTO, - type MenstrualCycleDTO, -} from "@/lib/cycle/dto"; -import { isModuleEnabled } from "@/lib/modules/gate"; +import { pullSyncChanges } from "@/lib/sync/changes/pull"; const DEFAULT_LIMIT = 200; const MAX_LIMIT = 500; -const DAY_MS = 86_400_000; const querySchema = z.object({ cursor: z.string().min(1).max(2048).optional(), limit: z.coerce.number().int().min(1).max(MAX_LIMIT).optional(), }); -interface MeasurementUpsert { - id: string; - externalId: string | null; - type: string; - value: number; - unit: string; - measuredAt: string; - source: string; - notes: string | null; - syncVersion: number; - updatedAt: string; -} - -interface MeasurementTombstone { - id: string; - externalId: string | null; - syncVersion: number; - deletedAt: string; - updatedAt: string; -} - -interface MoodUpsert { - id: string; - date: string; - mood: string; - score: number; - /** - * v1.38 — the five level-A self-state values, each 0-10 and each nullable. - * `a2` is inverse-oriented: a higher number means more stress, and a client - * that wants "up is better" flips the sign itself rather than the server - * storing a flipped value. - * - * Server-authoritative: `a1` is derived from the five-point label on every - * write, so a client that sends only `mood` still mirrors a complete row. - */ - a1: number | null; - a2: number | null; - a3: number | null; - a4: number | null; - a5: number | null; - /** - * v1.38 — the day's context, or null when the entry carries none. Null and - * an object of nulls are different answers and the feed keeps them apart. - */ - context: MoodContextWire | null; - tags: string | null; - note: string | null; - moodLoggedAt: string; - source: string; - syncVersion: number; - updatedAt: string; -} - -interface MoodTombstone { - id: string; - syncVersion: number; - deletedAt: string; - updatedAt: string; -} - -interface IntakeUpsert { - id: string; - medicationId: string; - scheduledFor: string; - takenAt: string | null; - skipped: boolean; - source: string; - /** v1.42.2 — recorded by the server (`autoMarkTaken`), not the person. */ - autoRecorded: boolean; - syncVersion: number; - updatedAt: string; -} - -interface IntakeTombstone { - id: string; - syncVersion: number; - deletedAt: string; - updatedAt: string; -} - -// v1.15.0 — cycle domains. cycleDays key tombstones on `externalId` when -// present (HealthKit-origin rows are externalId-keyed cross-device, like -// measurements) else the server `id`; cycles key on the server `id` -// (computed rows carry no externalId). -interface CycleDayTombstone { - id: string; - externalId: string | null; - syncVersion: number; - deletedAt: string; - updatedAt: string; -} - -interface CycleTombstone { - id: string; - syncVersion: number; - deletedAt: string; - updatedAt: string; -} - -/** - * Build the `(updatedAt, id)` keyset predicate for a domain's watermark. - * Empty object (full scan) when the client has no watermark for it yet. - */ -function keysetFilter(wm: DomainWatermark | undefined) { - if (!wm) return {}; - const at = new Date(wm.updatedAtMs); - return { - OR: [ - { updatedAt: { gt: at } }, - { AND: [{ updatedAt: at }, { id: { gt: wm.id } }] }, - ], - }; -} - export const GET = apiHandler(async (request: NextRequest) => { const { user } = await requireAuth(); @@ -232,424 +91,13 @@ export const GET = apiHandler(async (request: NextRequest) => { 422, ); } - const limit = parsed.data.limit ?? DEFAULT_LIMIT; - const serverNow = new Date(); - - // ── module gate: cycle ──────────────────────────────────────────── - // The `cycleDays` / `cycles` domains carry period days and per-day - // symptom rows — the same data every `/api/cycle/*` route refuses with a - // 403 when the module is off. Without this the delta feed re-served them - // ungated to the very same token. - // - // OMIT, NOT 403 — deliberately, and unlike the whole-record FHIR export. - // This feed multiplexes five domains over one cursor: refusing the whole - // request would wedge measurement / mood / intake catch-up for every - // account with cycle tracking off, which (the gate delegates to - // `isCycleEnabled`) is the default for most accounts. A partial payload - // is the only outcome that keeps the client syncing while still honouring - // the promise that a disabled module hands out nothing. - // - // The two cycle watermarks are deliberately NOT advanced while the module - // is off (the queries are skipped, so `advanceCursor` sees an empty page - // and leaves the prior watermark untouched). Re-enabling therefore resumes - // from where the client actually left off instead of silently skipping the - // rows that changed while the module was hidden. - // - // KNOWN GAP, out of scope here: a client that synced cycle rows BEFORE the - // module was turned off keeps its stale local mirror. Emitting tombstones - // for them would be wrong — a tombstone means deleted, and disabling a - // module must leave the rows intact so re-enabling finds a complete - // history. Purging a paired client's mirror needs an explicit - // "module went dark" signal in the feed contract plus client support for - // it; that is an additive wire change and an iOS-client change, not a - // server-side gate fix, so it is left for a coordinated contract cycle. - const cycleEnabled = await isModuleEnabled(user.id, "cycle"); - - // The retention horizon: tombstones older than this may have been - // pruned by the cleanup job, so a cursor that predates it can no longer - // be trusted to deliver every deletion incrementally. - const retentionHorizon = new Date( - serverNow.getTime() - TOMBSTONE_RETENTION_DAYS * DAY_MS, + return apiSuccess( + await pullSyncChanges({ + userId: user.id, + token: parsed.data.cursor, + limit: parsed.data.limit ?? DEFAULT_LIMIT, + serverNow: new Date(), + }), ); - - // A garbage / unparseable / old-format cursor decodes to null → fresh - // initial sync (every domain scans from the start). - const cursor: SyncCursor = parsed.data.cursor - ? (decodeCursor(parsed.data.cursor) ?? {}) - : {}; - - // A stale cursor (ANY domain watermark older than retention) forces a - // clean re-init: a deletion older than retention may already be pruned. - const pastRetention = SYNC_DOMAINS.some((domain) => { - const wm = cursor[domain]; - return wm !== undefined && wm.updatedAtMs < retentionHorizon.getTime(); - }); - // A cursor issued before the account's record was last replaced (a backup - // restore, or the person deleting all their data) cannot be caught up - // either: the restore wrote rows back with their original `updatedAt`, - // behind the cursor's watermarks, and removed rows without tombstones. The - // client re-initialises instead. Compared by the time the cursor was - // issued, not by its watermarks, which a restore leaves meaningless; a - // cursor from before the issue time was recorded counts as issued before - // any reset. - let beforeReset = false; - if (parsed.data.cursor) { - const account = await prisma.user.findUnique({ - where: { id: user.id }, - select: { syncResetAt: true }, - }); - if (account?.syncResetAt) { - const issuedAt = decodeCursorIssuedAt(parsed.data.cursor); - beforeReset = - issuedAt === null || issuedAt < account.syncResetAt.getTime(); - } - } - const expired = pastRetention || beforeReset; - if (expired) { - annotate({ - action: { name: "sync.changes.pull" }, - meta: { - cursor_expired: true, - cursor_expired_reason: beforeReset ? "sync_reset" : "retention", - returned: 0, - }, - }); - return apiSuccess({ - serverNow: serverNow.toISOString(), - cursor: parsed.data.cursor ?? null, - hasMore: false, - cursorExpired: true, - changes: { - measurements: { upserts: [], tombstones: [] }, - mood: { upserts: [], tombstones: [] }, - intakes: { upserts: [], tombstones: [] }, - cycleDays: { upserts: [], tombstones: [] }, - cycles: { upserts: [], tombstones: [] }, - }, - }); - } - - // Per-domain keyset walks. Both live and soft-deleted rows are in the - // same scan — a soft-delete bumps `updatedAt`, so a tombstone is just a - // row whose `deletedAt` is non-null. Fetch limit+1 to detect `hasMore` - // without a count query. - const [measurementRows, moodRows, intakeRows, cycleDayRows, cycleRows] = - await Promise.all([ - prisma.measurement.findMany({ - where: { userId: user.id, ...keysetFilter(cursor.measurements) }, - orderBy: [{ updatedAt: "asc" }, { id: "asc" }], - take: limit + 1, - select: { - id: true, - externalId: true, - type: true, - value: true, - unit: true, - measuredAt: true, - source: true, - notes: true, - notesEncrypted: true, - syncVersion: true, - deletedAt: true, - updatedAt: true, - }, - }), - prisma.moodEntry.findMany({ - where: { userId: user.id, ...keysetFilter(cursor.mood) }, - orderBy: [{ updatedAt: "asc" }, { id: "asc" }], - take: limit + 1, - select: { - id: true, - date: true, - mood: true, - score: true, - moodA1: true, - stressA2: true, - energyA3: true, - connectionA4: true, - stabilityA5: true, - context: true, - tags: true, - note: true, - noteEncrypted: true, - moodLoggedAt: true, - source: true, - syncVersion: true, - deletedAt: true, - updatedAt: true, - }, - }), - prisma.medicationIntakeEvent.findMany({ - where: { userId: user.id, ...keysetFilter(cursor.intakes) }, - orderBy: [{ updatedAt: "asc" }, { id: "asc" }], - take: limit + 1, - select: { - id: true, - medicationId: true, - scheduledFor: true, - takenAt: true, - skipped: true, - source: true, - autoRecorded: true, - syncVersion: true, - deletedAt: true, - updatedAt: true, - }, - }), - // Skipped outright when the module is off — not filtered after the - // fact — so the disabled rows are never read out of Postgres at all. - cycleEnabled - ? prisma.cycleDayLog.findMany({ - where: { userId: user.id, ...keysetFilter(cursor.cycleDays) }, - orderBy: [{ updatedAt: "asc" }, { id: "asc" }], - take: limit + 1, - include: dayLogSymptomInclude, - }) - : [], - cycleEnabled - ? prisma.menstrualCycle.findMany({ - where: { userId: user.id, ...keysetFilter(cursor.cycles) }, - orderBy: [{ updatedAt: "asc" }, { id: "asc" }], - take: limit + 1, - }) - : [], - ]); - - const nextCursor: SyncCursor = { ...cursor }; - let hasMore = false; - - // ── measurements ────────────────────────────────────────── - const mHasMore = measurementRows.length > limit; - const mPage = mHasMore ? measurementRows.slice(0, limit) : measurementRows; - const measurementUpserts: MeasurementUpsert[] = []; - const measurementTombstones: MeasurementTombstone[] = []; - // v1.42 — compaction tombstones are not deletions anyone made. The folds - // used to soft-delete the raw Apple Health samples they folded into an - // hourly or daily `stats:` row, and those rows rode this feed as - // tombstones for 75 days. They are left out now, by the same classifier the - // backlog purge uses (`folded-window.ts`); the cursor still advances past - // them, so the keyset walk is unchanged. A person's own deletions are - // reported exactly as before. - const compaction = new Set(); - const tombstonesOnPage = mPage.filter((row) => row.deletedAt !== null); - if (tombstonesOnPage.length > 0) { - const candidates = tombstonesOnPage.map((row) => ({ - type: row.type, - source: row.source, - externalId: row.externalId, - measuredAt: row.measuredAt, - deletedAt: row.deletedAt, - })); - if ( - candidates.some((row) => isFoldedWindowCandidate(row, row.deletedAt!)) - ) { - const classA = await findCompactionTombstones( - prisma, - user.id, - await loadFoldTimezone(prisma, user.id), - candidates, - ); - for (const index of classA) compaction.add(tombstonesOnPage[index].id); - } - } - for (const row of mPage) { - if (compaction.has(row.id)) continue; - if (row.deletedAt) { - measurementTombstones.push({ - id: row.id, - externalId: row.externalId, - syncVersion: row.syncVersion, - deletedAt: row.deletedAt.toISOString(), - updatedAt: row.updatedAt.toISOString(), - }); - } else { - // Every row carries its `source`. RECOVERY_SCORE is written by BOTH the - // WHOOP-native sync (`source = WHOOP`) and the COMPUTED proxy job - // (`source = COMPUTED`) for the same day; the client must resolve to ONE - // canonical recovery series — WHOOP wins when present, COMPUTED is the - // fallback — and never chart the proxy and the native value as two - // competing series. The feed faithfully mirrors both rows (a delta-sync - // mirror cannot drop a row mid-keyset without a tombstone); the - // canonical pick is the same WHOOP-over-COMPUTED rule the server's - // wellness tile + doctor PDF apply (`resolveCanonicalRecovery`). - measurementUpserts.push({ - id: row.id, - externalId: row.externalId, - type: row.type, - value: row.value, - unit: row.unit, - measuredAt: row.measuredAt.toISOString(), - source: row.source, - notes: readNote(row.notesEncrypted, row.notes), - syncVersion: row.syncVersion, - updatedAt: row.updatedAt.toISOString(), - }); - } - } - advanceCursor(nextCursor, "measurements", mPage); - hasMore = hasMore || mHasMore; - - // ── mood ────────────────────────────────────────────────── - const moodHasMore = moodRows.length > limit; - const moodPage = moodHasMore ? moodRows.slice(0, limit) : moodRows; - const moodUpserts: MoodUpsert[] = []; - const moodTombstones: MoodTombstone[] = []; - for (const row of moodPage) { - if (row.deletedAt) { - moodTombstones.push({ - id: row.id, - syncVersion: row.syncVersion, - deletedAt: row.deletedAt.toISOString(), - updatedAt: row.updatedAt.toISOString(), - }); - } else { - moodUpserts.push({ - id: row.id, - date: row.date, - mood: row.mood, - score: row.score, - // Under the same wire keys the write path accepts, so a client reads - // back exactly what it can post. - ...levelAForWire(row), - context: row.context ? contextForWire(row.context) : null, - tags: row.tags, - note: readNote(row.noteEncrypted, row.note), - moodLoggedAt: row.moodLoggedAt.toISOString(), - source: row.source, - syncVersion: row.syncVersion, - updatedAt: row.updatedAt.toISOString(), - }); - } - } - advanceCursor(nextCursor, "mood", moodPage); - hasMore = hasMore || moodHasMore; - - // ── intakes ─────────────────────────────────────────────── - const intakeHasMore = intakeRows.length > limit; - const intakePage = intakeHasMore ? intakeRows.slice(0, limit) : intakeRows; - const intakeUpserts: IntakeUpsert[] = []; - const intakeTombstones: IntakeTombstone[] = []; - for (const row of intakePage) { - if (row.deletedAt) { - intakeTombstones.push({ - id: row.id, - syncVersion: row.syncVersion, - deletedAt: row.deletedAt.toISOString(), - updatedAt: row.updatedAt.toISOString(), - }); - } else { - intakeUpserts.push({ - id: row.id, - medicationId: row.medicationId, - scheduledFor: row.scheduledFor.toISOString(), - takenAt: row.takenAt?.toISOString() ?? null, - skipped: row.skipped, - source: row.source, - autoRecorded: row.autoRecorded, - syncVersion: row.syncVersion, - updatedAt: row.updatedAt.toISOString(), - }); - } - } - advanceCursor(nextCursor, "intakes", intakePage); - hasMore = hasMore || intakeHasMore; - - // ── cycleDays ───────────────────────────────────────────── - const cycleDayHasMore = cycleDayRows.length > limit; - const cycleDayPage = cycleDayHasMore - ? cycleDayRows.slice(0, limit) - : cycleDayRows; - const cycleDayUpserts: CycleDayLogDTO[] = []; - const cycleDayTombstones: CycleDayTombstone[] = []; - for (const row of cycleDayPage) { - if (row.deletedAt) { - cycleDayTombstones.push({ - id: row.id, - externalId: row.externalId, - syncVersion: row.syncVersion, - deletedAt: row.deletedAt.toISOString(), - updatedAt: row.updatedAt.toISOString(), - }); - } else { - cycleDayUpserts.push(toCycleDayLogDTO(row)); - } - } - advanceCursor(nextCursor, "cycleDays", cycleDayPage); - hasMore = hasMore || cycleDayHasMore; - - // ── cycles ──────────────────────────────────────────────── - const cycleHasMore = cycleRows.length > limit; - const cyclePage = cycleHasMore ? cycleRows.slice(0, limit) : cycleRows; - const cycleUpserts: MenstrualCycleDTO[] = []; - const cycleTombstones: CycleTombstone[] = []; - for (const row of cyclePage) { - if (row.deletedAt) { - cycleTombstones.push({ - id: row.id, - syncVersion: row.syncVersion, - deletedAt: row.deletedAt.toISOString(), - updatedAt: row.updatedAt.toISOString(), - }); - } else { - cycleUpserts.push(toMenstrualCycleDTO(row)); - } - } - advanceCursor(nextCursor, "cycles", cyclePage); - hasMore = hasMore || cycleHasMore; - - annotate({ - action: { name: "sync.changes.pull" }, - meta: { - measurement_upserts: measurementUpserts.length, - measurement_tombstones: measurementTombstones.length, - mood_upserts: moodUpserts.length, - mood_tombstones: moodTombstones.length, - intake_upserts: intakeUpserts.length, - intake_tombstones: intakeTombstones.length, - cycle_day_upserts: cycleDayUpserts.length, - cycle_day_tombstones: cycleDayTombstones.length, - cycle_upserts: cycleUpserts.length, - cycle_tombstones: cycleTombstones.length, - cycle_module_enabled: cycleEnabled, - has_more: hasMore, - cursor_present: Boolean(parsed.data.cursor), - }, - }); - - return apiSuccess({ - serverNow: serverNow.toISOString(), - cursor: encodeCursor(nextCursor, serverNow.getTime()), - hasMore, - cursorExpired: false, - changes: { - measurements: { - upserts: measurementUpserts, - tombstones: measurementTombstones, - }, - mood: { upserts: moodUpserts, tombstones: moodTombstones }, - intakes: { upserts: intakeUpserts, tombstones: intakeTombstones }, - cycleDays: { - upserts: cycleDayUpserts, - tombstones: cycleDayTombstones, - }, - cycles: { upserts: cycleUpserts, tombstones: cycleTombstones }, - }, - }); }); - -/** - * Advance a domain's watermark to the last row of its page. A domain - * that returned no rows keeps its prior watermark untouched so a future - * pull does not re-scan from the start. - */ -function advanceCursor( - cursor: SyncCursor, - domain: SyncDomain, - page: Array<{ updatedAt: Date; id: string }>, -): void { - const last = page[page.length - 1]; - if (last) { - cursor[domain] = { updatedAtMs: last.updatedAt.getTime(), id: last.id }; - } -} diff --git a/src/app/api/workouts/[id]/route.ts b/src/app/api/workouts/[id]/route.ts index 274abaefd..2cfe031fa 100644 --- a/src/app/api/workouts/[id]/route.ts +++ b/src/app/api/workouts/[id]/route.ts @@ -29,76 +29,16 @@ */ import { NextRequest } from "next/server"; -import { prisma } from "@/lib/db"; import { apiHandler, requireAuth } from "@/lib/api-handler"; import { apiError, apiSuccess, getClientIp } from "@/lib/api-response"; import { annotate } from "@/lib/logging/context"; -import { auditLog } from "@/lib/auth/audit"; -import { invalidateUserMeasurements } from "@/lib/cache/invalidate"; -import { enqueuePrDetection } from "@/lib/jobs/pr-detection"; -import { - lockPersonalRecordsForUser, - workoutPrSlotsForSport, -} from "@/lib/personal-records/pr-detection-worker"; -import { - canonicalClusterOf, - canonicalWorkoutsInRange, - readWorkoutsAroundRange, - type CanonicalReadRow, - type WorkoutBoundedRead, - type WorkoutStartBounds, -} from "@/lib/workouts/canonical-read"; import { requireModuleEnabled } from "@/lib/modules/gate"; import { aiCapabilityToServe } from "@/lib/ai/capabilities/gate"; -import { getAgeFromDateOfBirth } from "@/lib/analytics/pulse-targets"; -import { decryptFromBytes } from "@/lib/ai/coach/bytes-codec"; -import { buildWorkoutHrSeries } from "@/lib/workouts/hr-series"; -import { - computeZones, - hrMaxFromAge, - parseWhoopZoneDurations, -} from "@/lib/workouts/zones"; -import { computeSplits } from "@/lib/workouts/splits"; -import { buildSportContext } from "@/lib/workouts/sport-context"; -import type { RouteCoordinate } from "@/lib/workouts/route-svg"; -import { readRouteGeometry } from "@/lib/workouts/route-geometry-cipher"; -import { resolveUserTimezone } from "@/lib/tz/resolver"; -import { userDayKey } from "@/lib/tz/format"; +import { readWorkoutDetail } from "@/lib/workouts/detail"; +import { deleteManualWorkout } from "@/lib/workouts/delete-manual-workout"; type RouteParams = { params: Promise<{ id: string }> }; -/** How many earlier rows one step of the previous-session search reads. */ -const PREVIOUS_SESSION_BATCH = 20; -/** Steps before the search gives up; each reaches past the last one. */ -const PREVIOUS_SESSION_MAX_STEPS = 5; - -/** - * The newest canonical workout of the sport that starts before `before` (the - * first start of the current session's cluster). A batch of earlier rows names - * the span to resolve; the span is read through the windowed reader so its - * twins are resolved the way the list resolves them. A span whose rows are all - * hidden by a twin further back moves the search back one batch. - */ -async function previousCanonicalWorkoutId( - read: WorkoutBoundedRead, - latestStartsBefore: (to: Date, take: number) => Promise, - before: Date, - priorityJson: unknown, -): Promise { - let to = before; - for (let step = 0; step < PREVIOUS_SESSION_MAX_STEPS; step += 1) { - const earlier = await latestStartsBefore(to, PREVIOUS_SESSION_BATCH); - if (earlier.length === 0) return null; - const from = earlier[earlier.length - 1]!; - const windowed = await readWorkoutsAroundRange({ from, to: before }, read); - const canonical = canonicalWorkoutsInRange(windowed, priorityJson); - const newest = canonical[canonical.length - 1]; - if (newest) return newest.id; - to = from; - } - return null; -} - export const GET = apiHandler( async (request: NextRequest, { params }: RouteParams) => { const { user } = await requireAuth(); @@ -119,283 +59,19 @@ export const GET = apiHandler( // not even loaded, `aiInsight` is null, and `ai` says why. const ai = await aiCapabilityToServe(user.id, "workoutInsights"); - const row = await prisma.workout.findUnique({ - where: { id }, - include: { - route: { - select: { - id: true, - geometry: true, - geometryEncrypted: true, - sampleTimestamps: true, - createdAt: true, - }, - }, - samples: { - select: { - samples: true, - sampleCount: true, - }, - }, - insight: ai.available - ? { - select: { paragraphEncrypted: true, generatedAt: true }, - } - : false, - }, + const body = await readWorkoutDetail({ + userId: user.id, + id, + compact, + ai, }); - // Cross-user 404 guard. The user can only fetch rows they own; any // other id surfaces as "not found" so the existence channel stays // sealed. - if (!row || row.userId !== user.id) { + if (!body) { return apiError("Workout not found", 404); } - - // Resolve the canonical winner for the row's cluster. The read goes - // through the same windowed reader as the list, so the cluster drawn here - // is the one the list draws: a twin that straddles a clock slot boundary, - // or one whose cluster begins with an earlier workout, resolves to the row - // the list shows. - const readSameSport = (bounds: WorkoutStartBounds) => - prisma.workout.findMany({ - where: { - userId: user.id, - sportType: row.sportType, - startedAt: bounds, - }, - orderBy: [{ startedAt: "asc" }, { id: "asc" }], - select: { - id: true, - source: true, - startedAt: true, - sportType: true, - // Part of the canonical session identity consulted by the picker — - // without it a HealthKit re-send of this very workout stays in the - // cluster and the deep link can resolve to the duplicate instead of - // the row the list shows. - durationSec: true, - avgHeartRate: true, - maxHeartRate: true, - metadata: true, - }, - }); - const around = await readWorkoutsAroundRange( - { from: row.startedAt, to: row.startedAt, inclusiveEnd: true }, - readSameSport, - ); - - const userRow = await prisma.user.findUnique({ - where: { id: user.id }, - select: { sourcePriorityJson: true, dateOfBirth: true }, - }); - // The local calendar day this session belongs to. Resolved here, in the - // user's timezone, so the day linkage stays server-authoritative — a - // client that derived it from `startedAt` would fall back to the browser - // zone and put a late-evening session on the wrong day whenever the two - // disagree. - const timezone = await resolveUserTimezone(user.id); - const dayKey = userDayKey(row.startedAt, timezone); - - const cluster = canonicalClusterOf( - around, - row.id, - userRow?.sourcePriorityJson ?? null, - ); - // The cluster's canonical rows, the requested row first when it is one of - // them and otherwise the nearest in time, so a deep link to a hidden twin - // lands on the most similar row. - const canonical = cluster?.canonical[0] ?? null; - const effectiveAvgHeartRate = canonical?.avgHeartRate ?? row.avgHeartRate; - const effectiveMaxHeartRate = canonical?.maxHeartRate ?? row.maxHeartRate; - - // ── Enrichment reads (all over existing tables — no migration) ──── - - // Heart-rate curve: stored series first, pulse-window fallback - // second, hide third. One server path, one DTO with provenance. - const hrSeries = await buildWorkoutHrSeries({ - userId: user.id, - startedAt: row.startedAt, - endedAt: row.endedAt, - durationSec: row.durationSec, - storedSamples: row.samples?.samples ?? null, - }); - annotate({ - action: { name: "workouts.detail.hr_series" }, - meta: { - source: hrSeries?.source ?? "none", - points: hrSeries?.points.length ?? 0, - }, - }); - - // Effort zones: WHOOP device durations win; else %HRmax fold from - // the series when profile age exists; else null. - const ageYears = getAgeFromDateOfBirth(userRow?.dateOfBirth ?? null); - const zones = computeZones({ - hrMax: hrMaxFromAge(ageYears), - series: hrSeries?.points ?? [], - bucketSec: hrSeries?.bucketSec ?? 0, - whoopZoneDurations: parseWhoopZoneDurations( - canonical?.metadata ?? row.metadata, - ), - }); - - // Per-km splits computed server-side from the geometry + timestamps - // so the web client keeps dropping the raw timestamp blob under - // `compact=1` while still rendering splits (server-authoritative - // parity — iOS gets the same resolved figures). - // v1.39.4 — the track is stored sealed; the legacy readable column is - // read only for a row the backfill has not reached yet. - const routeGeometry = row.route ? readRouteGeometry(row.route) : null; - const geometryCoords = - routeGeometry && - typeof routeGeometry === "object" && - Array.isArray((routeGeometry as { coordinates?: unknown }).coordinates) - ? (routeGeometry as { coordinates: RouteCoordinate[] }).coordinates - : null; - const splits = - geometryCoords && Array.isArray(row.route?.sampleTimestamps) - ? computeSplits(geometryCoords, row.route.sampleTimestamps as string[]) - : null; - - // Sport context: the user's own last-180-days average for this - // sport, cross-source-collapsed so twins don't double-count. - const sportContext = await buildSportContext( - user.id, - row.sportType, - userRow?.sourcePriorityJson ?? null, - row.id, - ); - - // The session before this one in the same sport. "How does this compare - // to last time?" is the most likely next question after reading a - // workout, and today it costs a trip back to the list plus a scan. - // Scoped to rows that start before this session's cluster, so a twin of - // THIS session can never be offered as the previous one, and resolved - // through the same windowed read so the link lands on the row the list - // shows. - const clusterStart = cluster?.members[0]?.startedAt ?? row.startedAt; - const previousWorkoutId = await previousCanonicalWorkoutId( - readSameSport, - async (to, take) => - ( - await prisma.workout.findMany({ - where: { - userId: user.id, - sportType: row.sportType, - startedAt: { lt: to }, - }, - orderBy: [{ startedAt: "desc" }, { id: "desc" }], - take, - select: { startedAt: true }, - }) - ).map((earlier) => earlier.startedAt), - clusterStart, - userRow?.sourcePriorityJson ?? null, - ); - - // A route whose track cannot be read (a sealed value that no longer - // opens, or an old row stored as a JSON null) is reported as no route at - // all: clients decode `route.geometry` as required, so a route object - // with a null geometry would fail the whole workout detail for them. - const route = - row.route && routeGeometry - ? { - geometry: routeGeometry, - // `compact=1` drops the (up to 20k-entry) timestamp array; the - // SVG needs geometry, not the per-sample timestamps, and the - // splits above are already derived from them server-side. - sampleTimestamps: compact - ? null - : (row.route.sampleTimestamps ?? null), - } - : null; - - // v1.10.0 — route-independent per-workout HR series. Present for - // both indoor (no route) and outdoor workouts that shipped a - // `samples` array on ingest; null otherwise. `compact=1` keeps the - // denormalised count but drops the raw sample blob (the web curve - // reads `hrSeries` instead). - const samples = row.samples - ? { - sampleCount: row.samples.sampleCount, - samples: compact ? null : row.samples.samples, - } - : null; - - // Decrypt the stored paragraph. Fail-SOFT here and only here: `decrypt` is - // fail-closed by design, so a rotated-away key would otherwise turn the - // whole workout-detail page into a 500 over a garnish field. An - // undecryptable paragraph degrades to no card, exactly like a workout that - // never had one. - let aiInsight: { paragraph: string; generatedAt: string } | null = null; - if (ai.available && row.insight) { - try { - aiInsight = { - paragraph: decryptFromBytes(row.insight.paragraphEncrypted), - generatedAt: row.insight.generatedAt.toISOString(), - }; - } catch { - annotate({ - action: { name: "workouts.detail.insight_undecryptable" }, - meta: { workoutId: row.id }, - }); - } - } - - return apiSuccess({ - id: row.id, - sportType: row.sportType, - startedAt: row.startedAt, - endedAt: row.endedAt, - // `YYYY-MM-DD` in the user's timezone. Additive; it addresses the - // day-scoped reads (intraday pulse, that night's sleep, the day's - // mood) the detail surface renders around the session. - dayKey, - durationSec: row.durationSec, - distanceM: row.totalDistanceM, - activeEnergyKcal: row.totalEnergyKcal, - avgHr: effectiveAvgHeartRate, - maxHr: effectiveMaxHeartRate, - minHr: row.minHeartRate, - // The row's own heart rate. `avgHr` / `maxHr` above may be filled in - // from a twin by the canonical picker, which is right for display and - // wrong for an edit: re-posting them would store the twin's values on - // the hand-entered row (#1162). - storedAvgHr: row.avgHeartRate, - storedMaxHr: row.maxHeartRate, - stepCount: row.stepCount, - elevationM: row.elevationM, - pauseDurationSec: row.pauseDurationSec, - source: row.source, - externalId: row.externalId, - metadata: row.metadata, - route, - samples, - // #67 enrichment — all additive, all over existing tables. - hrSeries, - zones, - splits, - sportContext, - // The per-workout Activity Insight. A pure READ of a row the - // `workout-insight-generate` worker wrote when this workout landed — - // this route never generates, never enqueues, and never falls back to - // a provider. A workout with no row (every historical one, every - // re-synced one, every one on a provider-less install) serves null and - // the page's `{aiInsight ? : null}` renders nothing. - aiInsight, - // Whether the Activity Insight can be shown, and why not. - ai, - // v1.4.32 — when the requested id is a non-canonical twin the - // caller can redirect to `canonicalId` to land on the cluster - // winner. `canonicalId === id` when the requested row already - // is the winner. - canonicalId: canonical?.id ?? row.id, - // The canonical previous session in this sport, or null when this is - // the first one on record. Additive; a client that does not read it is - // unaffected. - previousWorkoutId, - }); + return apiSuccess(body); }, ); @@ -425,89 +101,21 @@ export const DELETE = apiHandler( const { user } = await requireAuth(); const { id } = await params; - const row = await prisma.workout.findUnique({ - where: { id }, - select: { - id: true, - userId: true, - source: true, - externalId: true, - startedAt: true, - sportType: true, - }, + const outcome = await deleteManualWorkout({ + userId: user.id, + id, + ipAddress: getClientIp(request), }); - if (!row || row.userId !== user.id) { + if (outcome.kind === "not_found") { return apiError("Workout not found", 404); } - if (row.source !== "MANUAL") { - annotate({ - action: { - name: "workout.delete", - entity_type: "workout", - entity_id: id, - }, - meta: { outcome: "synced_source", source: row.source }, - }); + if (outcome.kind === "synced_source") { return apiError( "Only a workout entered by hand can be deleted here", 409, { errorCode: "workout.delete.synced_source" }, ); } - - const removedRecords = await prisma.$transaction(async (tx) => { - // The detector reads the best workout and writes its record under this - // same lock, so it either wrote before this delete (and the record goes - // below) or reads after it (and never sees the workout). - await lockPersonalRecordsForUser(tx, user.id); - const records = await tx.personalRecord.deleteMany({ - where: { - userId: user.id, - source: "MANUAL", - ...(row.externalId !== null - ? { metricSlot: { not: null }, externalId: row.externalId } - : { - // A legacy record carries no external id, so the start time - // is all that ties it to this workout. Scope it to the slots - // of this workout's sport so a record another sport set at - // the same instant stays. - metricSlot: { in: workoutPrSlotsForSport(row.sportType) }, - externalId: null, - achievedAt: row.startedAt, - }), - }, - }); - await tx.workout.delete({ where: { id: row.id } }); - return records.count; - }); - - await auditLog("workout.delete", { - userId: user.id, - ipAddress: getClientIp(request), - details: { - workoutId: id, - sportType: row.sportType, - personalRecordsRemoved: removedRecords, - }, - }); - annotate({ - action: { name: "workout.delete", entity_type: "workout", entity_id: id }, - meta: { outcome: "deleted", personalRecordsRemoved: removedRecords }, - }); - - invalidateUserMeasurements(user.id, { evict: true }); - - if (removedRecords > 0) { - try { - await enqueuePrDetection(user.id, { silent: true }); - } catch (err) { - annotate({ - action: { name: "personal_records.detection_enqueue_failed" }, - meta: { error: err instanceof Error ? err.message : String(err) }, - }); - } - } - return apiSuccess({ deleted: true }); }, ); diff --git a/src/lib/analytics/__tests__/glucose-panel-window.test.ts b/src/lib/analytics/__tests__/glucose-panel-window.test.ts index 56bfa8e60..dd4071fce 100644 --- a/src/lib/analytics/__tests__/glucose-panel-window.test.ts +++ b/src/lib/analytics/__tests__/glucose-panel-window.test.ts @@ -20,7 +20,7 @@ import { const ROOT = join(__dirname, "..", "..", "..", ".."); const CONSUMERS = [ - "src/app/api/analytics/route.ts", + "src/lib/analytics/response/glucose.ts", "src/lib/dashboard/snapshot.ts", // v1.28.26 — the Coach glucose section moved into its own block module; // the alias derivation lives there and snapshot.ts imports the alias. diff --git a/src/lib/analytics/compliance/__tests__/as-needed-compliance-exclusion.test.ts b/src/lib/analytics/compliance/__tests__/as-needed-compliance-exclusion.test.ts index af4b53233..af62e8406 100644 --- a/src/lib/analytics/compliance/__tests__/as-needed-compliance-exclusion.test.ts +++ b/src/lib/analytics/compliance/__tests__/as-needed-compliance-exclusion.test.ts @@ -31,7 +31,7 @@ const CALL_SITES: Array<{ file: string; surface: string }> = [ surface: "insight targets", }, { - file: "src/app/api/insights/comprehensive/route.ts", + file: "src/lib/insights/comprehensive-overview/medication-compliance.ts", surface: "comprehensive insight", }, { diff --git a/src/lib/analytics/response/bp-in-target.ts b/src/lib/analytics/response/bp-in-target.ts new file mode 100644 index 000000000..5ebf062c4 --- /dev/null +++ b/src/lib/analytics/response/bp-in-target.ts @@ -0,0 +1,188 @@ +/** + * The blood-pressure in-target block of the default analytics body: the + * reporting-window percentages against the user's effective targets, and + * the envelopes the Health Score grades against the clinical band. + */ +import type { SessionUser } from "@/lib/auth/session-user"; +import { resolveEffectiveBpTargets } from "@/lib/analytics/effective-range"; +import { bpTargetsEqual, getBpTargets } from "@/lib/analytics/bp-targets"; +import { + computeBpInTargetFastPath, + type BpInTargetEnvelope, +} from "@/lib/analytics/bp-in-target-fast-path"; +import { deriveBpWindow90 } from "@/lib/analytics/window-confidence"; +import type { probeRollupCoverage } from "@/lib/rollups/measurement-coverage"; + +type Coverage = Awaited>; + +export async function readBpInTargetBlock( + user: SessionUser, + coverage: Coverage, + userTz: string, +) { + // BP in-target percentage (auto-calculated from date of birth) + let bpInTargetPct: number | null = null; + let bpInTargetPct7d: number | null = null; + let bpInTargetPct30d: number | null = null; + let bpInTargetPctAllTime: number | null = null; + /** + * Period-aligned prior-window + * pcts so the BD-Zielbereich tile's comparison-overlay caption + * stops mismatching its math with its label. The tile's + * `compareDelta` is `last30Days - priorMonth` (or `… - priorYear`) + * matching the user's `comparisonBaseline` selection, never + * `last30Days - allTime` (the v1.4.22 A2 shortcut). + */ + let bpInTargetPctPriorMonth: number | null = null; + let bpInTargetPctPriorYear: number | null = null; + // 90-day pair count + effective label span for the tile's + // thin-data gate. Surfaced so the BD-Zielbereich tile renders "collecting + // data" until the window holds enough readings, with a dynamic span label. + let bpInTargetCount90: number | null = null; + let bpInTargetSpanDays90: number | null = null; + // Hold the current + prior-week BP envelopes so the shared + // Health-Score input builder grades the pillar off the identical shape the + // dashboard snapshot uses (one builder, no per-surface assembly drift). + // The graded score + prior-week BP figures the score consumes are read off + // these envelopes inside `buildHealthScoreBpInputs`. + let bpEnvelope: BpInTargetEnvelope | null = null; + let bpEnvelopePriorWeek: BpInTargetEnvelope | null = null; + let bpEnvelopePriorTwoWeeks: BpInTargetEnvelope | null = null; + const bpTargets = resolveEffectiveBpTargets( + { + dateOfBirth: user.dateOfBirth, + gender: user.gender, + heightCm: user.heightCm ?? null, + }, + user.thresholdsJson, + ); + const clinicalBpTargets = getBpTargets(user.dateOfBirth); + if (bpTargets) { + const now = new Date(); + // Probe-gated dispatcher replaces the inline chunked + // read. When BOTH BP types have DAY-bucket coverage the helper + // pairs the per-day MEAN SYS + MEAN DIA from `measurement_rollups` + // and counts in-target days against the five reporting windows, + // skipping the heavy 365-day chunked walk against `measurements`. + // Falls back to the legacy `computeBpInTargetWindows` over a + // chunked read when coverage is partial so a brand-new account + // (no buckets yet) still sees per-event numbers. The helper emits + // a `path: "rollup" | "live"` annotate so prod logs prove which + // branch fired. See `bp-in-target-fast-path.ts` for the + // documented per-day-mean approximation. + // + // Two parallel runs: one anchored at `now` for the + // standard windows, one anchored at `now - 7d` so the Health-Score + // helper has a real prior-week BP pct to feed into its previous- + // window snapshot. The two probes share the coverage map and the + // same rollup / live branch decision; the extra call is one extra + // pair of `readRollupBuckets` reads on the rollup path and is a + // no-op when the user has no BP rows at all. + const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const fourteenDaysAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000); + const [windows, windowsPriorWeek, windowsPriorTwoWeeks] = await Promise.all( + [ + computeBpInTargetFastPath({ + userId: user.id, + targets: bpTargets, + now, + coverage, + // Cross-tz runtime guard. The helper falls back + // to the live SQL path when the user is more than 3 hours + // from UTC, where the rollup table's UTC-midnight day-key + // would slip a calendar day relative to the live aggregator's + // window cuts. + userTz, + }), + computeBpInTargetFastPath({ + userId: user.id, + targets: bpTargets, + now: sevenDaysAgo, + coverage, + userTz, + }), + computeBpInTargetFastPath({ + userId: user.id, + targets: bpTargets, + now: fourteenDaysAgo, + coverage, + userTz, + }), + ], + ); + // The pillar is graded against the CLINICAL band even when the user + // keeps a personal target (that band is the number's identity; the + // personal one is displayed beside it and scores nothing). So when the + // two differ we re-run the whole helper against the clinical targets + // and the score comes from that second run. + // + // The "why this number" basis rides the same envelope, so it + // is the same run, the same representative pair and the same ceilings + // by construction. Assign envelopes WHOLE here; picking `graded.score` + // off one run and a basis off the other would let the popover explain + // a number nobody is looking at, and it would look correct in review. + if (bpTargetsEqual(bpTargets, clinicalBpTargets)) { + bpEnvelope = windows; + bpEnvelopePriorWeek = windowsPriorWeek; + bpEnvelopePriorTwoWeeks = windowsPriorTwoWeeks; + } else if (clinicalBpTargets) { + [bpEnvelope, bpEnvelopePriorWeek, bpEnvelopePriorTwoWeeks] = + await Promise.all([ + computeBpInTargetFastPath({ + userId: user.id, + targets: clinicalBpTargets, + now, + coverage, + userTz, + }), + computeBpInTargetFastPath({ + userId: user.id, + targets: clinicalBpTargets, + now: sevenDaysAgo, + coverage, + userTz, + }), + computeBpInTargetFastPath({ + userId: user.id, + targets: clinicalBpTargets, + now: fourteenDaysAgo, + coverage, + userTz, + }), + ]); + } + // The BD-Zielbereich headline standardises on the + // trailing-90-day window (labelled "· 90 T" in the tile). All-time + // remains carried below for the BP detail page's long view only. + bpInTargetPct = windows.last90Days?.pct ?? null; + bpInTargetPct7d = windows.last7Days?.pct ?? null; + bpInTargetPct30d = windows.last30Days?.pct ?? null; + bpInTargetPctAllTime = windows.allTime?.pct ?? null; + bpInTargetPctPriorMonth = windows.priorMonth?.pct ?? null; + bpInTargetPctPriorYear = windows.priorYear?.pct ?? null; + // Count + effective span for the BD-Zielbereich tile's + // confidence gate (thin-data → "collecting data"; dynamic span label). + // Shared helper keeps this identical to the dashboard snapshot. + const bpWindow = deriveBpWindow90( + windows.last90Days, + windows.last90EarliestAt, + now, + ); + bpInTargetCount90 = bpWindow.count; + bpInTargetSpanDays90 = bpWindow.spanDays; + } + return { + bpInTargetPct, + bpInTargetPct7d, + bpInTargetPct30d, + bpInTargetPctAllTime, + bpInTargetPctPriorMonth, + bpInTargetPctPriorYear, + bpInTargetCount90, + bpInTargetSpanDays90, + bpEnvelope, + bpEnvelopePriorWeek, + bpEnvelopePriorTwoWeeks, + clinicalBpTargets, + }; +} diff --git a/src/lib/analytics/response/correlations.ts b/src/lib/analytics/response/correlations.ts new file mode 100644 index 000000000..61a7b71e9 --- /dev/null +++ b/src/lib/analytics/response/correlations.ts @@ -0,0 +1,92 @@ +/** + * The correlation block of the default analytics body: the three fixed + * hypotheses, with the accepted ones synced to the stored patterns and + * their decision attached. + */ +import type { Locale } from "@/lib/i18n/config"; +import { computeCorrelationHypothesesFastPath } from "@/lib/analytics/correlations-fast-path"; +import { + decisionForEvidence, + PATTERN_FAMILIES, + syncAcceptedPatterns, +} from "@/lib/insights/correlation-patterns"; +import type { probeRollupCoverage } from "@/lib/rollups/measurement-coverage"; + +type Coverage = Awaited>; + +export async function readCorrelationBlock(args: { + userId: string; + userTz: string; + coverage: Coverage; + locale: Locale; +}) { + const { userId, userTz, coverage, locale } = args; + // Three pre-defined correlation hypotheses. + // Probe-gated helper. The 28-day scan window (down + // from 30 to keep the cold critical path tight while still + // satisfying the n >= 20 surface gate) reads SYS / PULSE / WEIGHT + // per-day means from `measurement_rollups` when the user has full + // coverage; mood and medication-intake reads always stay live (no + // rollup equivalent). The helper emits `meta.correlations.path` + + // `meta.correlations.window_days` so prod logs prove the branch + // selection and the truthful window. + const correlations = await computeCorrelationHypothesesFastPath({ + userId, + userTz, + now: new Date(), + coverage, + locale, + }); + const fixedDefinitions = [ + { + key: "bpCompliance" as const, + factorKey: "HYPOTHESIS:MEDICATION_COMPLIANCE", + outcomeKey: "BLOOD_PRESSURE_SYS", + }, + { + key: "moodPulse" as const, + factorKey: "HYPOTHESIS:MOOD", + outcomeKey: "RESTING_PULSE", + }, + { + key: "weightWeekday" as const, + factorKey: "HYPOTHESIS:WEEKDAY", + outcomeKey: "WEIGHT", + }, + ]; + const acceptedFixed = fixedDefinitions.flatMap((definition) => { + const result = correlations[definition.key]; + return result.status === "ok" + ? [ + { + factorKey: definition.factorKey, + outcomeKey: definition.outcomeKey, + lagDays: 0, + sampleSize: result.n, + effectSize: result.statistic, + pValue: result.pValue, + qValue: null, + }, + ] + : []; + }); + const fixedDecisions = await syncAcceptedPatterns({ + userId, + family: PATTERN_FAMILIES.fixed, + accepted: acceptedFixed, + }); + const correlationsWithPatterns = { ...correlations }; + for (const definition of fixedDefinitions) { + const result = correlations[definition.key]; + if (result.status !== "ok") continue; + const decision = decisionForEvidence(fixedDecisions, { + factorKey: definition.factorKey, + outcomeKey: definition.outcomeKey, + lagDays: 0, + }); + correlationsWithPatterns[definition.key] = decision + ? { ...result, ...decision } + : result; + } + return correlationsWithPatterns; +} diff --git a/src/lib/analytics/response/default-body.ts b/src/lib/analytics/response/default-body.ts new file mode 100644 index 000000000..4ba36afe1 --- /dev/null +++ b/src/lib/analytics/response/default-body.ts @@ -0,0 +1,173 @@ +/** + * The default `GET /api/analytics` body, built once per (user, locale) and + * cached by the route: the module-gated summaries, BMI, the blood-pressure + * in-target block, the glucose blocks, the correlations, the Health Score and + * the sleep stages. The stages run in the order the route always ran them. + */ +import type { SessionUser } from "@/lib/auth/session-user"; +import { annotate } from "@/lib/logging/context"; +import type { Locale } from "@/lib/i18n/config"; +import { DEFAULT_TIMEZONE } from "@/lib/tz/resolver"; +import { readSummariesSlice } from "@/lib/analytics/summaries-slice"; +import { ensureUserRollupsFresh } from "@/lib/rollups/measurement-rollups"; +import { probeRollupCoverage } from "@/lib/rollups/measurement-coverage"; +import { resolveModuleMap } from "@/lib/modules/gate"; +import { gateSummariesByModules } from "@/lib/dashboard/widget-modules"; + +import { readBpInTargetBlock } from "./bp-in-target"; +import { readCorrelationBlock } from "./correlations"; +import { readGlucoseBlocks } from "./glucose"; +import { readHealthScoreBlock } from "./health-score"; +import { computeSleepStageBreakdown } from "./sleep-stages"; + +export type AnalyticsBody = Awaited>; + +/** + * The heavy default-slice body, outside the route handler so `cachedSwr()` + * can wrap it. Returns the raw JSON payload; the route attaches the + * `Cache-Control` header afterward. + */ +export async function buildAnalyticsResponse( + user: SessionUser, + locale: Locale, +) { + // Fire-and-forget the rollup-fresh check. + // Awaiting it on the read path can stall the Node event loop for + // 30–60 s on a power-user account whose iOS step samples keep the + // 90-day DAY window slightly stale, which starves /api/health, + // /api/version, and concurrent iOS calls. The downstream coverage + // probe falls back to live SQL when a type is uncovered, so + // correctness is preserved; the worst the user sees is data from + // up to ~60 s ago on the very first request after a fresh + // measurement lands, and the next request returns the up-to-date + // value once the background refresh completes. `ensureUserRollupsFresh` + // wraps its own try/catch, so the void call cannot reject. + void ensureUserRollupsFresh(user.id); + + // Single per-type coverage probe shared by the + // bp_in_target / healthScore / correlations branches below. The + // probe is one indexed query against `measurement_rollups`; the + // three downstream branches each reuse the resulting `coverage` + // map to decide between the rollup-fast-path and the live + // fallback. Probing once instead of per-branch keeps the fan-out + // cost flat across the three branches. + const coverage = await probeRollupCoverage(user.id); + + // Every day-bucket call inside this route honours the + // user's display timezone via `userDayKey(date, userTz)` (the legacy + // `berlinDayKey()` helper was retired in v1.4.40). + const userTz = user.timezone ?? DEFAULT_TIMEZONE; + + // Replace the 15-way per-type `fetchMeasurementSeriesChunked` + // fan-out with the slim rollup-tier reader. The previous live walk + // against `measurements` (425-day window, ≥ 50 Prisma round-trips on a + // 15-type tenant) saturated the `p-limit(4)` slot pool for ~8 s on the + // production 467 k-row account — and because Prisma's 20-slot pool was + // simultaneously claimed by those slots, the concurrent + // `?slice=summaries` request (only ~9 small queries) queued behind it + // and observed the same ~8 s wall-clock. + // + // `computeSummariesSlice` reads `measurement_rollups` DAY buckets + // (sub-second) plus one 90-day narrow `$queryRaw` for the windowed + // avg / slope / r² columns, and falls back to a split-aggregate live + // path for users with incomplete rollup coverage. Response shape is + // identical to the previous fan-out output for every field the + // dashboard actually consumes: + // - `count / latest / min / max / mean` from DAY buckets + // - `avg7 / avg30 / slope7 / slope30` from the narrow query + // - `avg30LastMonth` from the same narrow query (added in this + // release; previously only the live walk produced it) + // - `avg30LastYear` populated for any type whose WMY tier carries + // the year-ago window + // - `lastSeenByType` from the per-type latest read + // + // `anomalyCount` and `slope90` left this wire in v1.37.19: the slim + // path could never compute the former (it always reported 0) and no + // client component read the latter — the insights pipeline computes + // both from its own `summarize()` / comprehensive-aggregator reads. + // The one field the slim path leaves at a default value is + // `avg30LastYear` on types with no WMY coverage, which matches the + // pre-fix behaviour for tenants whose year-ago window happened to + // fall outside the 425-day floor. + const slim = await readSummariesSlice(user.id); + // Same module gate as the slim slice and the snapshot builder — the + // default slice feeds the dashboard too (it is the thick half of the + // legacy two-cell path), so it cannot be the one feed that leaks a + // disabled module's types. + const thickModules = await resolveModuleMap(user.id); + const thickGated = gateSummariesByModules( + slim.summaries, + slim.lastSeenByType, + thickModules, + ); + const results = thickGated.summaries; + const lastSeenByType = thickGated.lastSeenByType; + + // `computeSummariesSlice` annotates `action: "analytics.get.slim"` + // for telemetry on the slim route. Restore the default-slice action + // name so the wide-event reflects the route the client actually + // called (otherwise every default-slice request would appear in the + // logs under the slim action). + annotate({ action: { name: "analytics.get" } }); + + // Sleep-stage breakdown for the trailing 30 days. Only + // included when the user has stage-tagged rows in window; null + // otherwise so the UI can render a plain total without the + // breakdown card painting empty. + const sleepStages = await computeSleepStageBreakdown(user.id, userTz); + + // BMI calculation + let bmi: number | null = null; + if (user.heightCm && results.WEIGHT?.latest) { + const heightM = user.heightCm / 100; + bmi = Math.round((results.WEIGHT.latest / (heightM * heightM)) * 10) / 10; + } + + const bp = await readBpInTargetBlock(user, coverage, userTz); + const { glucoseByContext, glucoseClinical } = await readGlucoseBlocks( + user.id, + ); + const correlationsWithPatterns = await readCorrelationBlock({ + userId: user.id, + userTz, + coverage, + locale, + }); + const { glucoseEnabled, healthScore } = await readHealthScoreBlock({ + user, + userTz, + clinicalBpTargets: bp.clinicalBpTargets, + bpEnvelope: bp.bpEnvelope, + bpEnvelopePriorWeek: bp.bpEnvelopePriorWeek, + bpEnvelopePriorTwoWeeks: bp.bpEnvelopePriorTwoWeeks, + }); + const glucoseByContextOut = glucoseEnabled ? glucoseByContext : null; + const glucoseClinicalOut = glucoseEnabled ? glucoseClinical : null; + + return { + summaries: results, + bmi, + bpInTargetPct: bp.bpInTargetPct, + bpInTargetPct7d: bp.bpInTargetPct7d, + bpInTargetPct30d: bp.bpInTargetPct30d, + bpInTargetPctAllTime: bp.bpInTargetPctAllTime, + bpInTargetPctPriorMonth: bp.bpInTargetPctPriorMonth, + bpInTargetPctPriorYear: bp.bpInTargetPctPriorYear, + bpInTargetCount90: bp.bpInTargetCount90, + bpInTargetSpanDays90: bp.bpInTargetSpanDays90, + glucoseByContext: glucoseByContextOut, + glucoseClinical: glucoseClinicalOut, + correlations: correlationsWithPatterns, + healthScore, + sleepStages, + // The latest night's source-discrepancy annotation, computed + // inside the slim slice's per-night post-pass. Additive: the dashboard + // sleep tile marks the headline with a discreet "sources disagree" + // hint; clients that don't read the field stay unchanged. + sleepSourceDiscrepancy: slim.sleepSourceDiscrepancy, + // Per-type freshness map drives the dashboard's + // staleness caption on each ``. Additive: clients that + // don't read the field stay unchanged. + lastSeenByType, + }; +} diff --git a/src/lib/analytics/response/glucose.ts b/src/lib/analytics/response/glucose.ts new file mode 100644 index 000000000..10a812eff --- /dev/null +++ b/src/lib/analytics/response/glucose.ts @@ -0,0 +1,60 @@ +/** + * The glucose blocks of the default analytics body: the per-context + * summaries and the clinical panel over the same trailing window. + */ +import { prisma } from "@/lib/db"; +import { summarize, wireSummary, type DataPoint } from "@/lib/analytics/trends"; +import { + computeGlucoseClinicalMetrics, + GLUCOSE_PANEL_WINDOW_DAYS, + type GlucoseClinicalMetrics, +} from "@/lib/analytics/glucose-metrics"; +import { groupByGlucoseContext } from "@/lib/glucose"; + +export async function readGlucoseBlocks(userId: string) { + // Per-context glucose summaries (canonical mg/dL). + // + // Bound to the trailing 30 days. The dashboard tile + // path only reads the trailing window; pre-bound this query + // walked every persisted BG row a multi-year user has written. + const glucoseSince = new Date( + Date.now() - GLUCOSE_PANEL_WINDOW_DAYS * 24 * 60 * 60 * 1000, + ); + const glucoseRows = await prisma.measurement.findMany({ + where: { + userId, + type: "BLOOD_GLUCOSE", + measuredAt: { gte: glucoseSince }, + deletedAt: null, + }, + orderBy: { measuredAt: "asc" }, + select: { value: true, measuredAt: true, glucoseContext: true }, + }); + // #943 — grouped through the shared bucket resolver, so readings with no + // meal-time tag land in `UNSPECIFIED` instead of being dropped on the floor. + const glucoseByContext: Record> = {}; + for (const [bucket, ctxRows] of groupByGlucoseContext( + glucoseRows, + (r) => r.glucoseContext, + )) { + glucoseByContext[bucket] = wireSummary( + summarize( + ctxRows.map((r): DataPoint => ({ date: r.measuredAt, value: r.value })), + ), + ); + } + + // Server-authoritative glucose clinical panel. The TIR / GMI / + // eA1C / CV% headline and the advanced J-index + LBGI/HBGI tier are computed + // ONCE here over the declared 30-day window (matching `glucoseSince`) so the + // insights panel, the coach snapshot, and the doctor report all render the + // same numbers instead of each re-deriving. The learning gate keeps a thin + // spot-data window from being asserted as a clinical AGP. Always present in + // the payload (even with zero readings) so the client renders the calm + // "still learning" state from a populated object rather than a missing key. + const glucoseClinical: GlucoseClinicalMetrics = computeGlucoseClinicalMetrics( + glucoseRows.map((r) => ({ measuredAt: r.measuredAt, mgdl: r.value })), + { windowDays: GLUCOSE_PANEL_WINDOW_DAYS, now: new Date() }, + ); + return { glucoseByContext, glucoseClinical }; +} diff --git a/src/lib/analytics/response/health-score.ts b/src/lib/analytics/response/health-score.ts new file mode 100644 index 000000000..e3c9818a2 --- /dev/null +++ b/src/lib/analytics/response/health-score.ts @@ -0,0 +1,89 @@ +/** + * The Health Score block of the default analytics body: the versioned + * cardiometabolic reference report, recorded, with the Rest Mode framing. + */ +import type { SessionUser } from "@/lib/auth/session-user"; +import type { BpInTargetEnvelope } from "@/lib/analytics/bp-in-target-fast-path"; +import type { getBpTargets } from "@/lib/analytics/bp-targets"; +import { computeAndRecordUserHealthScore } from "@/lib/analytics/score/record"; +import { isModuleEnabled } from "@/lib/modules/gate"; +import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; +import { resolveRestMode } from "@/lib/illness/rest-mode"; +import { resolveGlucoseUnit } from "@/lib/glucose"; + +export async function readHealthScoreBlock(args: { + user: SessionUser; + userTz: string; + clinicalBpTargets: ReturnType; + bpEnvelope: BpInTargetEnvelope | null; + bpEnvelopePriorWeek: BpInTargetEnvelope | null; + bpEnvelopePriorTwoWeeks: BpInTargetEnvelope | null; +}) { + const { + user, + userTz, + clinicalBpTargets, + bpEnvelope, + bpEnvelopePriorWeek, + bpEnvelopePriorTwoWeeks, + } = args; + // The versioned cardiometabolic reference report. The shared reader + // admits only evidence-backed pillars with their own windows and coverage + // floors, then applies the three-domain breadth rule. Its BP envelopes come + // from the same rollup/live fast path the dashboard uses; every other domain + // resolves through the canonical reader and fails independently. + const [ + glucoseEnabled, + labsEnabled, + sleepEnabled, + mentalHealthEnabled, + scoreSourcePriority, + ] = await Promise.all([ + isModuleEnabled(user.id, "glucose"), + isModuleEnabled(user.id, "labs"), + isModuleEnabled(user.id, "sleep"), + isModuleEnabled(user.id, "mentalHealth"), + loadUserSourcePriority(user.id), + ]); + const scoredAt = new Date(); + const healthScore = await computeAndRecordUserHealthScore({ + userId: user.id, + now: scoredAt, + profile: { + dateOfBirth: user.dateOfBirth, + heightCm: user.heightCm ?? null, + timezone: userTz, + sourcePriorityJson: scoreSourcePriority, + thresholdsJson: user.thresholdsJson ?? null, + glucoseUnit: resolveGlucoseUnit(user.glucoseUnit), + }, + modules: { + glucose: glucoseEnabled, + labs: labsEnabled, + sleep: sleepEnabled, + mentalHealth: mentalHealthEnabled, + }, + healthScoreConfigJson: user.healthScoreConfigJson, + bpTargets: clinicalBpTargets, + bpEnvelope, + bpEnvelopePriorWeek, + bpEnvelopePriorTwoWeeks, + }); + + // Rest Mode annotation. When an illness/condition episode is + // active we frame the score (not penalise it): the number above is left + // exactly as computed and we attach a value-free "you were unwell" context. + // Resolved server-side so iOS mirrors the same fact, never recomputing it. + // Resolved as-of the scored window (`scoredAt`), not a fresh now. + if (healthScore) { + const restMode = await resolveRestMode(user.id, scoredAt); + healthScore.restMode = restMode.active + ? { + active: true, + since: restMode.since, + episodeCount: restMode.episodeCount, + } + : null; + } + return { glucoseEnabled, healthScore }; +} diff --git a/src/lib/analytics/response/last-seen.ts b/src/lib/analytics/response/last-seen.ts new file mode 100644 index 000000000..547cebdc0 --- /dev/null +++ b/src/lib/analytics/response/last-seen.ts @@ -0,0 +1,37 @@ +/** + * Derive `daysAgo` from the cached `lastSeenAt` ISO string per request. The + * cached envelope deliberately omits `daysAgo` so the dashboard tile + * staleness caption stays correct across day boundaries even when the body + * has been sitting in the 60s LRU. + * + * Returns a shallow-copied envelope with the enriched `lastSeenByType` map; + * everything else is passed through unchanged. + */ +export function enrichLastSeenDaysAgo< + T extends { + lastSeenByType: Record; + }, +>( + body: T, +): Omit & { + lastSeenByType: Record< + string, + { lastSeenAt: string; daysAgo: number } | null + >; +} { + const nowMs = Date.now(); + const enriched: Record< + string, + { lastSeenAt: string; daysAgo: number } | null + > = {}; + for (const [type, slot] of Object.entries(body.lastSeenByType)) { + if (slot === null) { + enriched[type] = null; + continue; + } + const lastMs = new Date(slot.lastSeenAt).getTime(); + const daysAgo = Math.floor((nowMs - lastMs) / (24 * 60 * 60 * 1000)); + enriched[type] = { lastSeenAt: slot.lastSeenAt, daysAgo }; + } + return { ...body, lastSeenByType: enriched }; +} diff --git a/src/lib/analytics/response/sleep-stages.ts b/src/lib/analytics/response/sleep-stages.ts new file mode 100644 index 000000000..60a72beca --- /dev/null +++ b/src/lib/analytics/response/sleep-stages.ts @@ -0,0 +1,69 @@ +/** + * The sleep-stage block of the default analytics body. + */ +import { prisma } from "@/lib/db"; +import { + buildSleepStageComposition, + type SleepStageComposition, +} from "@/lib/analytics/sleep-stage-composition"; +import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; + +/** + * Per-stage sleep-minutes breakdown over the trailing 30 days. + * + * Returns `null` when the user has no stage-tagged sleep rows in + * window — the analytics consumer renders the existing + * `summaries.SLEEP_DURATION` totals without a stage card in that + * case. Returns the sum-per-stage AND the day count covered so the + * UI can render an "averaged across N nights" caption truthfully. + * + * Also returns a `perNight` array: one entry per + * Berlin-tz day inside the window, with minutes-per-stage for the + * stacked-bar chart. Days with zero stage-tagged rows are omitted + * from `perNight` (the chart treats them as gaps). + * + * The per-night stages are the MAIN NIGHT's stages. A nap ends on the + * same wake day as that morning's night, and the per-wake-day totals + * folded it into the night's buckets — an afternoon nap added CORE to + * the night and the column read longer and shallower than the night + * was. The nap's minutes now ride alongside on `napMinutes` / + * `napCount`, both absent on a day without one. + */ +export async function computeSleepStageBreakdown( + userId: string, + userTz: string, +): Promise { + const DAY_MS = 24 * 60 * 60 * 1000; + const WINDOW_DAYS = 30; + const since = new Date(Date.now() - WINDOW_DAYS * DAY_MS); + // Read the FULL per-stage row set (not `sleepStage: { not: null }`) + // and route it through the canonical session reconstruction so the + // stacked-bar agrees with the dashboard tile + chart series. The old path + // keyed each stage row by its OWN calendar day (splitting a midnight- + // spanning night across two buckets) and never collapsed multi-source + // (double-counting a WHOOP + Apple Health night). Routing through the + // helper fixes both: it session-clusters, keys by the wake day, and picks + // one canonical source per night. The bare-vs-granular de-dup is applied + // per-session to the per-stage breakdown too. The `source` is selected so + // the helper's source-collapse can run. + const rows = await prisma.measurement.findMany({ + where: { + userId, + type: "SLEEP_DURATION", + measuredAt: { gte: since }, + deletedAt: null, + }, + select: { + value: true, + measuredAt: true, + sleepStage: true, + source: true, + deviceType: true, + }, + }); + + if (rows.length === 0) return null; + + const priorityJson = await loadUserSourcePriority(userId); + return buildSleepStageComposition(rows, userTz, priorityJson, WINDOW_DAYS); +} diff --git a/src/lib/auth/oidc-callback/complete-sign-in.ts b/src/lib/auth/oidc-callback/complete-sign-in.ts new file mode 100644 index 000000000..8635c414a --- /dev/null +++ b/src/lib/auth/oidc-callback/complete-sign-in.ts @@ -0,0 +1,116 @@ +/** + * The three ways an SSO sign-in ends once its account is known + * (`GET /api/auth/oidc/callback`): this app's own second factor, a native + * handoff code, or a web session. The callback turns each into its + * redirect. + */ +import { prisma } from "@/lib/db"; +import type { User } from "@/generated/prisma/client"; +import { annotate } from "@/lib/logging/context"; +import { auditLog } from "@/lib/auth/audit"; +import { createSession } from "@/lib/auth/session"; +import { recordSignInDevice } from "@/lib/auth/login-alert"; +import { createMfaChallenge } from "@/lib/auth/mfa/challenge"; +import { syncMfaEnrollCookie } from "@/lib/auth/mfa-enrollment"; +import { mintNativeHandoff } from "@/lib/auth/oidc-native-handoff"; + +export type MfaMethod = "totp" | "recovery" | "webauthn"; + +/** + * OIDC is a DELEGATED factor: it proves the IdP authenticated someone, but it + * never substitutes for this app's own second factor. An account with a + * confirmed TOTP secret or a registered security key gets the same MFA + * challenge step password login gets: no session is minted, and the + * single-use ticket goes to the login page (web) or the custom scheme + * (native), which render the same challenge UI and complete at + * `/api/auth/mfa/verify`. (The trusted-device skip cannot apply on this + * path: its cookie is SameSite=Strict, which the browser withholds on an + * IdP-initiated redirect chain.) + * + * Answers the challenge, or null when the account has no second factor. + */ +export async function startOidcSecondFactor( + user: User, + ip: string, + native: boolean, +): Promise<{ ticket: string; methods: MfaMethod[] } | null> { + const hasTotp = Boolean(user.totpConfirmedAt); + const webauthnKeyCount = await prisma.webauthnMfaCredential.count({ + where: { userId: user.id }, + }); + const hasWebauthn = webauthnKeyCount > 0; + if (!hasTotp && !hasWebauthn) return null; + + const challenge = await createMfaChallenge(user.id, "login"); + const methods: MfaMethod[] = []; + if (hasTotp) methods.push("totp", "recovery"); + if (hasWebauthn) methods.push("webauthn"); + await auditLog("auth.mfa.challenge", { + userId: user.id, + ipAddress: ip, + details: { source: native ? "login.oidc.native" : "login.oidc" }, + }); + annotate({ + action: { name: "auth.mfa.challenge" }, + meta: { mfa_required: true }, + }); + return { ticket: challenge.ticket, methods }; +} + +/** + * Native, no MFA: a one-time handoff code for the app. NO session and NO + * cookie are minted here: the app exchanges the code at + * `POST /api/auth/oidc/native/token` for the native bundle. The app's S256 + * challenge (bound at login, tamper-authenticated in the state blob) locks + * the code to the app instance that started the flow. The session and + * device are recorded at the exchange via `finishLogin`, not here. + */ +export async function mintOidcNativeHandoff(input: { + userId: string; + appCodeChallenge: string; + ip: string; + userAgent: string | null; +}): Promise { + const { code } = await mintNativeHandoff({ + userId: input.userId, + appCodeChallenge: input.appCodeChallenge, + ipAddress: input.ip, + userAgent: input.userAgent, + }); + await auditLog("auth.oidc.native.handoff_minted", { + userId: input.userId, + ipAddress: input.ip, + }); + annotate({ action: { name: "auth.oidc.native.handoff_minted" } }); + return code; +} + +/** A web sign-in without a second factor: the session and its ledger row. */ +export async function openOidcSession( + user: User, + ip: string, + userAgent: string | null, +): Promise { + // Parity with password login: the admin-enforced-MFA hint cookie sends a + // single-factor account into forced enrollment after sign-in. + await syncMfaEnrollCookie(user.id, { + totpConfirmedAt: user.totpConfirmedAt, + mfaEnforced: user.mfaEnforced, + }); + + // The 5th argument stays null: OIDC is a delegated factor, so an SSO + // session must never satisfy `requireFreshMfa` step-up (MFA disable, + // encryption-key rotation, encrypted export, account deletion), the same + // treatment a trusted-device login gets. + await createSession( + user.id, + user.onboardingCompletedAt === null, + ip, + userAgent, + null, + ); + + void recordSignInDevice({ userId: user.id, ip, userAgent }); + + await auditLog("auth.oidc.login", { userId: user.id, ipAddress: ip }); +} diff --git a/src/lib/auth/oidc-callback/resolve-account.ts b/src/lib/auth/oidc-callback/resolve-account.ts new file mode 100644 index 000000000..cc9b7a39a --- /dev/null +++ b/src/lib/auth/oidc-callback/resolve-account.ts @@ -0,0 +1,202 @@ +/** + * Which account an SSO sign-in belongs to (`GET /api/auth/oidc/callback`), + * once the provider has authenticated a subject. + * + * 1. The durable identity match: the (issuer, sub) pair stamped on a + * previous login, link or provision. + * 2. An account that already holds the verified email: never linked on the + * email alone. Pinned to another identity it is a conflict; otherwise + * the link waits for the account's own credential. + * 3. Nobody: provision, while registration is open. + * + * A refusal names the reason the callback redirects with. + */ +import { prisma } from "@/lib/db"; +import type { User } from "@/generated/prisma/client"; +import { annotate } from "@/lib/logging/context"; +import { auditLog } from "@/lib/auth/audit"; +import { deriveUniqueUsername } from "@/lib/auth/oidc"; +import { resolveServerDefaultTimezone } from "@/lib/tz/resolver"; + +export type OidcAccountResolution = + | { kind: "user"; user: User } + | { kind: "refused"; reason: string } + | { kind: "linkPending"; userId: string }; + +/** A refusal; the reason is the `?error=` the callback redirects with. */ +function refuse(reason: string): OidcAccountResolution { + return { kind: "refused", reason }; +} + +interface OidcSubject { + issuer: string; + sub: string; + email: string | null; + emailVerified: boolean | undefined; + ip: string; +} + +/** + * Refresh the display email only, and only from a claim the IdP explicitly + * marks verified. Skipped when another account already holds the address + * (`email` is unique); the login itself proceeds either way, keyed by + * (issuer, sub). Email is deliberately NOT consulted for the match: after + * the stamp it is a display field, so an IdP-side email change can refresh + * it but can never re-point the login at a different account. + */ +async function refreshDisplayEmail( + user: User, + subject: OidcSubject, +): Promise { + const displayEmail = subject.email ? subject.email.toLowerCase() : null; + if ( + !displayEmail || + subject.emailVerified !== true || + user.email?.toLowerCase() === displayEmail + ) { + return user; + } + const emailTaken = await prisma.user.findFirst({ + where: { + email: { equals: displayEmail, mode: "insensitive" }, + NOT: { id: user.id }, + }, + select: { id: true }, + }); + if (emailTaken) return user; + return prisma.user.update({ + where: { id: user.id }, + data: { email: displayEmail }, + }); +} + +async function registrationOpen(): Promise { + const userCount = await prisma.user.count(); + try { + const settings = await prisma.appSettings.findUnique({ + where: { id: "singleton" }, + }); + if (settings && !settings.registrationEnabled && userCount > 0) { + return false; + } + } catch { + // Table may not exist yet; allow provisioning (matches register/route.ts). + } + return true; +} + +/** + * The first provisioned user becomes ADMIN. Deriving that role from an early + * count and then creating without coordination is the check-then-act race + * the password-register route closed: two concurrent first logins racing the + * empty-DB window would both observe `0` and both be minted ADMIN. The + * count+insert is serialised behind a transaction-scoped advisory lock + * (released on commit/rollback) and re-counted *inside* the lock, so the + * second provision observes the first's committed row and is minted USER. + * The lock key STRING matches the register route's: an OIDC first login + * racing a password first registration is serialised against the same lock, + * so only one ADMIN can ever be minted. + */ +async function provision( + subject: OidcSubject & { email: string }, +): Promise { + const username = await deriveUniqueUsername(subject.email, (candidate) => + prisma.user + .findUnique({ where: { username: candidate } }) + .then((u: unknown) => u !== null), + ); + // Same server-default resolution the register route uses: an + // SSO-provisioned account has no registration form to carry the + // browser-detected timezone. + const timezone = await resolveServerDefaultTimezone(); + const user = await prisma.$transaction(async (tx) => { + // `pg_advisory_xact_lock` returns void, which the client cannot + // deserialize as a column; selecting FROM it yields a plain int row. + await tx.$queryRaw` + SELECT 1 AS locked + FROM pg_advisory_xact_lock(hashtextextended('register:first-admin', 0)) + `; + const priorUsers = await tx.user.count(); + const role = priorUsers === 0 ? "ADMIN" : "USER"; + return tx.user.create({ + data: { + email: subject.email.toLowerCase(), + username, + passwordHash: null, + role, + timezone, + oidcIssuer: subject.issuer, + oidcSub: subject.sub, + }, + }); + }); + await auditLog("auth.oidc.provisioned", { + userId: user.id, + ipAddress: subject.ip, + }); + return user; +} + +export async function resolveOidcAccount( + subject: OidcSubject, +): Promise { + // (1) Durable identity match. + const stamped = await prisma.user.findFirst({ + where: { oidcIssuer: subject.issuer, oidcSub: subject.sub }, + }); + if (stamped) { + return { kind: "user", user: await refreshDisplayEmail(stamped, subject) }; + } + + // No stamped identity yet: both remaining paths (link-once, provision) + // bind this (issuer, sub) to an account keyed by email, so the claim must + // be explicitly `email_verified: true`. An ABSENT claim is a reject, not a + // benefit of the doubt: an IdP that does not assert verification cannot + // anchor a link that later gates a health record. + const email = subject.email; + if (!email) { + annotate({ meta: { reason: "no_email" } }); + return refuse("oidc_no_email"); + } + if (subject.emailVerified !== true) { + annotate({ meta: { reason: "email_unverified" } }); + return refuse("oidc_email_unverified"); + } + + const byEmail = await prisma.user.findFirst({ + where: { email: { equals: email, mode: "insensitive" } }, + }); + if (byEmail) { + // (2) Link ONCE. An account already pinned to a different IdP identity + // is never silently re-bound: a rebind would let a second IdP identity + // that acquires the same email capture the account. Audited so the + // operator can see the collision. + if (byEmail.oidcIssuer !== null || byEmail.oidcSub !== null) { + await auditLog("auth.oidc.link_conflict", { + userId: byEmail.id, + ipAddress: subject.ip, + }); + annotate({ meta: { reason: "identity_conflict" } }); + return refuse("oidc_identity_conflict"); + } + // (2b) Never link on the email alone. The local address was never + // verified (anyone can register with it or type it into a profile), so + // matching it proves nothing about who owns the account. Record the link + // this login would make; the callback sends the browser to sign in to + // the account with its own credential, and the link is made there + // (`completePendingOidcLink`), and only for this account. + await auditLog("auth.oidc.link_pending", { + userId: byEmail.id, + ipAddress: subject.ip, + }); + annotate({ meta: { reason: "link_confirmation_required" } }); + return { kind: "linkPending", userId: byEmail.id }; + } + + // (3) Provision. + if (!(await registrationOpen())) { + annotate({ meta: { reason: "registration_disabled" } }); + return refuse("oidc_registration_disabled"); + } + return { kind: "user", user: await provision({ ...subject, email }) }; +} diff --git a/src/lib/auth/oidc-callback/state.ts b/src/lib/auth/oidc-callback/state.ts new file mode 100644 index 000000000..1cc56fd3e --- /dev/null +++ b/src/lib/auth/oidc-callback/state.ts @@ -0,0 +1,102 @@ +/** + * The single-use state blob of an SSO sign-in, as the callback reads it back + * (`GET /api/auth/oidc/callback`), and the provider error codes it may name + * in its wide event. + */ +import { decrypt } from "@/lib/crypto"; + +export interface StoredOidcState { + state: string; + nonce: string; + codeVerifier: string; + next: string; + /** + * Set only when the login GET carried `client=native`. When true the + * callback mints a handoff code (or MFA ticket) and redirects to the custom + * scheme instead of minting a session. Both fields live ONLY inside the + * AES-256-GCM blob, so they are tamper-authenticated: a network attacker or + * the IdP cannot flip a web login into a native one or swap the app + * challenge. + */ + native?: boolean; + /** The app's S256 PKCE challenge, bound at login; verified at the exchange. */ + appCodeChallenge?: string; + /** + * Set only when the login carried `intent=claim` and a valid `hl_claim` + * cookie (#959). The callback then claims the managed profile the cookie + * names instead of signing in, and only when the cookie still carries this + * exact binding. Web only: never together with `native`. + */ + intent?: "claim"; + claimBinding?: string; +} + +function isStoredOidcState(value: unknown): value is StoredOidcState { + const v = value as Record; + if ( + typeof value !== "object" || + value === null || + typeof v.state !== "string" || + typeof v.nonce !== "string" || + typeof v.codeVerifier !== "string" || + typeof v.next !== "string" + ) { + return false; + } + // Optional native fields: when present they must be the right shape, and a + // native start MUST carry the challenge the exchange verifies against. + if (v.native !== undefined) { + if (v.native !== true) return false; + if (typeof v.appCodeChallenge !== "string") return false; + } + if (v.intent !== undefined) { + if (v.intent !== "claim") return false; + if (typeof v.claimBinding !== "string" || v.claimBinding.length === 0) { + return false; + } + if (v.native !== undefined) return false; + } + return true; +} + +/** The decrypted blob, or null when it is missing, unreadable or malformed. */ +export function readStoredOidcState( + rawCookie: string | undefined, +): StoredOidcState | null { + if (!rawCookie) return null; + try { + const parsed: unknown = JSON.parse(decrypt(rawCookie)); + return isStoredOidcState(parsed) ? parsed : null; + } catch { + return null; + } +} + +/** + * Closed set of RFC 6749 §4.1.2.1 / OIDC Core §3.1.2.6 authorization error + * codes. The wide-event meta only ever carries a member of this set (or + * "other"), never the raw, attacker-influenceable query value. + */ +const KNOWN_IDP_ERROR_CODES = new Set([ + "access_denied", + "invalid_request", + "unauthorized_client", + "unsupported_response_type", + "invalid_scope", + "server_error", + "temporarily_unavailable", + "interaction_required", + "login_required", + "account_selection_required", + "consent_required", + "invalid_request_uri", + "invalid_request_object", + "request_not_supported", + "request_uri_not_supported", + "registration_not_supported", +]); + +/** The provider's error code as the wide event may carry it. */ +export function idpErrorCode(raw: string): string { + return KNOWN_IDP_ERROR_CODES.has(raw) ? raw : "other"; +} diff --git a/src/lib/dashboard/summary/build.ts b/src/lib/dashboard/summary/build.ts new file mode 100644 index 000000000..0cd828a42 --- /dev/null +++ b/src/lib/dashboard/summary/build.ts @@ -0,0 +1,383 @@ +/** + * The body of `GET /api/dashboard/summary`, built on a cache miss: greeting, + * intake-day streak, today's medication compliance, and the metric cards + * with latest value, sparkline, trend and verdict, plus the sleep rhythm. + * + * Every per-type read is bounded (see `@/lib/dashboard/summary-reads`): one + * row per type for the latest reading, at most `SPARK_DAYS` DAY-rollup + * buckets per type for the sparkline, the all-time counts from the DAY + * rollups, and the set of local day keys with activity in the streak window. + */ +import { prisma } from "@/lib/db"; +import type { MeasurementType } from "@/generated/prisma/client"; +import { annotate } from "@/lib/logging/context"; +import { measurementTypeEnum } from "@/lib/validations/measurement"; +import { getServerTranslator } from "@/lib/i18n/server-translator"; +import { type Locale } from "@/lib/i18n/config"; +import { resolveJobLocale } from "@/lib/i18n/job-locale"; +import { userDayKey } from "@/lib/tz/resolver"; +import { startOfLocalDayKey } from "@/lib/tz/local-day"; +import { buildMedsTodayBlock } from "@/lib/dashboard/meds-today"; +import { computeStreak } from "@/lib/dashboard/streak"; +import { + readActivityDayKeys, + readAllTimeCounts, + readLatestEver, + readSparkBuckets, +} from "@/lib/dashboard/summary-reads"; +import { + summarizeSleepNights, + type SleepStageRow, +} from "@/lib/analytics/sleep-night"; +import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; +import { loadBaselineProfile } from "@/lib/insights/derived/baseline"; +import { computeBmi } from "@/lib/insights/derived/bmi"; +import { isDerivedOk } from "@/lib/insights/derived/types"; +import { buildMoodDailySeries } from "@/lib/analytics/mood-series"; +import { loadFreeWeekdaySpans } from "@/lib/profile/life-circumstances"; +import { + reconstructNights, + sleepNeedMinutes, +} from "@/lib/insights/derived/sleep-score"; +import { + computeSleepRhythmFromNights, + type SleepRhythmDto, +} from "@/lib/insights/derived/sleep-rhythm"; + +import { plainCard, SPARK_DAYS, withVerdicts, type MetricCard } from "./cards"; +import { summaryLookups, type SummaryLookups } from "./lookups"; +import { measurementCards } from "./measurement-cards"; + +const STREAK_WINDOW_DAYS = 365; + +export interface SummaryBuilderContext { + greetingName: string; + locale: Locale; +} + +export async function buildSummaryContext(user: { + displayName: string | null; + username: string; + locale: string | null; +}): Promise { + const greetingName = user.displayName ?? user.username; + // The greeting salutation and the streak label are the two strings this + // route still translates server-side, and the native client is its only + // caller: a Bearer request carries no locale cookie and no meaningful + // Accept-Language, so the stored column is the only per-user signal. That + // is exactly the shape `resolveJobLocale` exists for: stored user locale, + // then the operator's configured default, then English. + const locale: Locale = await resolveJobLocale(user.locale); + return { greetingName, locale }; +} + +/** + * Mood is the one metric on this payload that is not a `Measurement`. The + * daily series is the canonical `buildMoodDailySeries` output (rollup + * fast-path, 1..5 scale, one entry per day with an average score), the same + * series the web mood analytics and the dashboard snapshot render. Emitted + * only when the account has ever logged a mood. + */ +function moodCard( + moodSeries: Awaited>, + userTz: string, +): MetricCard | null { + if (moodSeries.entryCount === 0) return null; + const spark = moodSeries.entries.slice(-SPARK_DAYS).map((e) => e.score); + const latestEntry = moodSeries.entries[moodSeries.entries.length - 1]; + // The series carries the user's LOCAL day keys (`MoodEntry.date`), not + // timestamps, so the only honest instant is the first instant of that day + // in the user's zone. Writing the key as `T00:00Z` instead put the instant + // on the previous calendar day for anyone west of UTC. + const latestAt = latestEntry + ? startOfLocalDayKey(latestEntry.date, userTz).toISOString() + : null; + return plainCard("mood", "mood", { + latestValue: latestEntry?.score ?? null, + spark, + updatedAt: latestAt, + allTimeCount: moodSeries.entryCount, + lastSeenAt: latestAt, + }); +} + +/** + * BMI, derived from the latest weight at the profile height by + * `computeBmi`, the single implementation the web vitals dashboard also + * renders. Omitted when the derivation is insufficient (no height on the + * profile, or no weight inside its window): the card shape has no "unknown" + * state to express honestly, and a card with a null value would read as a + * logging gap rather than a missing profile field. + */ +function bmiCard( + bmiDerived: Awaited>, + lookups: SummaryLookups, +): MetricCard | null { + if (!isDerivedOk(bmiDerived)) return null; + const spark = bmiDerived.value.series.slice(-SPARK_DAYS); + // BMI moves exactly when weight does, so the weight metadata is the honest + // recency signal: there is no separate BMI reading. + return plainCard("bmi", "bmi", { + latestValue: bmiDerived.value.bmi, + spark, + updatedAt: lookups.metaForType("WEIGHT").lastSeenAt, + allTimeCount: lookups.metaForType("WEIGHT").allTimeCount, + lastSeenAt: lookups.metaForType("WEIGHT").lastSeenAt, + }); +} + +type Timed = (label: string, builder: () => Promise) => Promise; + +/** The summary's reads, in their three rounds, each timed under its label. */ +async function readSummaryInputs( + userId: string, + userTz: string, + now: Date, + time: Timed, +) { + // The latest-reading and sparkline reads take the most recent N rows per + // type regardless of age; only the streak window anchors on a calendar + // slice. + const streakWindowStart = new Date( + now.getTime() - STREAK_WINDOW_DAYS * 86_400_000, + ); + // Derived from the canonical enum so a new measurement type is + // auto-included. The card builders decide which types render as cards. + const measurementTypes = [ + ...measurementTypeEnum.options, + ] as MeasurementType[]; + + const [ + latestEver, + sparkBuckets, + allTimeAggregate, + medsToday, + streakActivity, + measurementStreakDays, + sleepStageRows, + ] = await Promise.all([ + // Both reads live in `@/lib/dashboard/summary-reads`: the SQL is a + // contract the integration suite exercises directly against Postgres, + // and each carries the reason it names the type list at its definition. + // Short version: PostgreSQL 16 has no index skip scan, so "one row per + // type" has to be driven FROM the type list or it degrades to a full + // pass over the account. + time("latestEver", () => readLatestEver(prisma, userId, measurementTypes)), + time("sparkline", () => + readSparkBuckets(prisma, userId, measurementTypes, SPARK_DAYS), + ), + // Counted from the DAY rollups where they cover the days, not by one + // index entry per reading (see `readAllTimeCounts`). + time("allTime", () => + readAllTimeCounts(prisma, userId, measurementTypes, now), + ), + // The shared `buildMedsTodayBlock` runs the same idempotent projection + // the intake route runs (`skipDuplicates: true` so a concurrent + // intake-route hit can't race a duplicate row in) before its + // today-window tally, so this route and the dashboard snapshot count the + // identical row set. Daily schedules have no row to read until the + // projection writes one. + time("medsToday", () => buildMedsTodayBlock(prisma, userId, userTz, now)), + time("streakIntakes", () => + prisma.medicationIntakeEvent.findMany({ + where: { + userId, + // Tombstoned rows do not count towards a streak. + deletedAt: null, + scheduledFor: { gte: streakWindowStart }, + OR: [{ takenAt: { not: null } }, { skipped: true }], + }, + select: { takenAt: true, scheduledFor: true }, + }), + ), + time("streakDays", () => + readActivityDayKeys(prisma, userId, userTz, streakWindowStart, now), + ), + // Raw per-stage SLEEP_DURATION rows for the night-total tile. + // `SLEEP_DURATION` is stored one row per STAGE per night (minutes), so + // the single most recent row (the `latestEver` path) is just ONE stage, + // not the night. Bounded to the streak window (about a year of ~5 + // rows/night); the headline only needs the most recent night but the + // window also feeds the sparkline night totals. + time("sleepNights", () => + prisma.measurement.findMany({ + where: { + userId, + type: "SLEEP_DURATION", + deletedAt: null, + measuredAt: { gte: streakWindowStart }, + }, + orderBy: { measuredAt: "asc" }, + // Writer-level collapse: two HealthKit apps behind one source + // (watch stages vs phone in-bed) must not blend into one night. + select: { + value: true, + measuredAt: true, + sleepStage: true, + source: true, + deviceType: true, + }, + }), + ), + ]); + + // The user's sleep source-priority ladder collapses a dual-source night + // (e.g. WHOOP + Apple Health) to one canonical source before the per-night + // reconstruction sums it. The baseline profile rides along (one read) so + // the sleep-rhythm block resolves the age-based sleep need. + const [sleepPriorityJson, sleepProfile, moodSeries] = await Promise.all([ + loadUserSourcePriority(userId), + loadBaselineProfile(prisma, userId), + // The mood card reads the SAME canonical daily series the web mood + // analytics and the dashboard snapshot read, rollup fast-path included: + // never a second aggregation. + time("moodSeries", () => buildMoodDailySeries(userId, prisma)), + ]); + + // BMI is derived, not measured: it needs the profile height loaded above, + // so it cannot join the round before it. + const bmiDerived = await time("bmi", () => + computeBmi(userId, sleepProfile, { now }), + ); + + return { + latestEver, + sparkBuckets, + allTimeAggregate, + medsToday, + streakActivity, + measurementStreakDays, + sleepStageRows, + sleepPriorityJson, + sleepProfile, + moodSeries, + bmiDerived, + }; +} + +export async function buildDashboardSummary( + userId: string, + userTz: string, + ctx: SummaryBuilderContext, +) { + const now = new Date(); + // Per-sub-query wall-clock timing, reported under + // `meta.dashboard.sub_*_ms` so a regression can be attributed to a + // specific sub-query without re-instrumenting the route. Captured on the + // cache-miss path only; a cache hit skips this whole builder. + const timings: Record = {}; + const time = async ( + label: string, + builder: () => Promise, + ): Promise => { + const t0 = Date.now(); + const result = await builder(); + timings[`dashboard.sub_${label}_ms`] = Date.now() - t0; + return result; + }; + + const { + latestEver, + sparkBuckets, + allTimeAggregate, + medsToday, + streakActivity, + measurementStreakDays, + sleepStageRows, + sleepPriorityJson, + sleepProfile, + moodSeries, + bmiDerived, + } = await readSummaryInputs(userId, userTz, now, time); + + const lookups = summaryLookups({ + latestEver, + sparkBuckets, + allTimeAggregate, + }); + + // The activity-day set from the bounded streak reads. The measurement + // side arrives as local YYYY-MM-DD keys, the same keys + // `userDayKey(measuredAt, userTz)` produces for the intakes. + const activityDays = new Set(measurementStreakDays); + for (const e of streakActivity) { + activityDays.add(userDayKey(e.takenAt ?? e.scheduledFor, userTz)); + } + const streak = computeStreak(activityDays, userTz); + + const sleepRows = sleepStageRows as SleepStageRow[]; + const sleepSummary = summarizeSleepNights( + sleepRows, + userTz, + sleepPriorityJson, + ); + + // Server-authoritative sleep-rhythm DTO (sleep debt + chronotype) off the + // SAME canonical reconstruction the Sleep page + Sleep Score read, so iOS + // renders identical values. Both signals carry a calm partial / learning + // state below their night thresholds, never an assertion off thin data. + const sleepRhythm: SleepRhythmDto = computeSleepRhythmFromNights( + reconstructNights(sleepRows, userTz, sleepPriorityJson), + sleepNeedMinutes(sleepProfile.ageYears), + "COMPUTED", + await loadFreeWeekdaySpans(userId), + ); + + const metrics = measurementCards(lookups, { + summary: sleepSummary, + rows: sleepRows, + tz: userTz, + priorityJson: sleepPriorityJson, + }); + const mood = moodCard(moodSeries, userTz); + if (mood) metrics.push(mood); + const bmi = bmiCard(bmiDerived, lookups); + if (bmi) metrics.push(bmi); + + const t = getServerTranslator(ctx.locale).t; + + // The record's profile and own targets, for the card verdicts. + const verdictProfile = await prisma.user.findUnique({ + where: { id: userId }, + select: { + dateOfBirth: true, + gender: true, + heightCm: true, + thresholdsJson: true, + }, + }); + + annotate({ meta: timings }); + + return { + greeting: { + salutation: t("dashboard.greetingSalutation", { name: ctx.greetingName }), + date: now.toISOString(), + }, + streak: { + currentDays: streak.currentDays, + longest: streak.longest, + label: t("dashboard.streakLabel"), + }, + // The shared builder carries the exact tally semantics this tile always + // had (scheduled = every today-window row; taken = `takenAt` set and not + // skipped). The wire shape of `compliance` is locked for iOS. + compliance: { + scheduledToday: medsToday.scheduledToday, + takenToday: medsToday.takenToday, + }, + highlightInsight: null, + metrics: withVerdicts(metrics, { + profile: { + dateOfBirth: verdictProfile?.dateOfBirth ?? null, + gender: verdictProfile?.gender ?? null, + heightCm: verdictProfile?.heightCm ?? null, + }, + thresholdsJson: verdictProfile?.thresholdsJson ?? null, + }), + // Sleep debt + chronotype, server-authoritative. Same values the Sleep + // page's `/api/sleep/rhythm` read returns. + sleepRhythm, + lastUpdated: now.toISOString(), + }; +} diff --git a/src/lib/dashboard/summary/cards.ts b/src/lib/dashboard/summary/cards.ts new file mode 100644 index 000000000..7c0d61e35 --- /dev/null +++ b/src/lib/dashboard/summary/cards.ts @@ -0,0 +1,307 @@ +/** + * The metric cards of `GET /api/dashboard/summary`: their wire shape, the + * i18n keys they carry, the trend and verdict laid over them, and the + * builders for each card the summary emits. + * + * The shape is fixed for the iOS client and intentionally normalised: + * `kind` is iOS-friendly (camelCase), unlike the canonical Prisma enum + * (BLOOD_PRESSURE_SYS etc.). + */ +import type { MeasurementType, SleepStage } from "@/generated/prisma/client"; +import { + resolveTrendVerdict, + trendSentimentOfTrend, + type TrendSentiment, + type TrendVerdict, + type TrendVerdictContext, +} from "@/lib/targets/trend-verdict"; +import { + reconstructSleepNights, + type SleepSourceDiscrepancy, + type SleepStageRow, +} from "@/lib/analytics/sleep-night"; + +export const SPARK_DAYS = 7; + +export type MetricKind = + | "weight" + | "bloodPressure" + | "pulse" + | "bodyFat" + | "glucose" + | "sleep" + | "steps" + | "totalBodyWater" + | "boneMass" + | "oxygenSaturation" + | "mood" + | "bmi"; + +export interface MetricCard { + id: string; + kind: MetricKind; + /** + * i18n key for the metric's display title (e.g. + * `dashboard.metric.title.weight`). Clients resolve the key against their + * own locale bundle: the PWA via `useTranslations()`, the iOS client via + * its bundled `Localizable.xcstrings`. The previous wire shape carried a + * German string literal which injected DE into an English-locale iOS UI. + */ + titleKey: string; + latestValue: number | null; + secondaryValue: number | null; + /** + * i18n key for the metric's display unit (e.g. + * `dashboard.metric.unit.weight`). Same resolution contract as + * `titleKey`. SI tokens like `kg`, `mmHg`, `bpm`, `mg/dL` are identical + * across every shipped locale; per-kind keys keep the door open for + * locale-specific overrides (e.g. Imperial units) without another + * wire-shape change. + */ + unitKey: string; + /** + * Explicit unit token for clients that need to know the value's unit + * without inferring it from the metric kind. Most tiles leave this `null` + * (the `unitKey` i18n key already carries the display unit). The `sleep` + * tile sets it to `"h"` because its `latestValue` is a per-NIGHT total + * expressed in HOURS (a float), not the canonical `SLEEP_DURATION` + * minutes. + */ + unit: string | null; + /** + * Per-stage hours for the headline night, sleep tile only. `null` for + * every other kind and for a sleep night with no stage-tagged rows (a + * legacy bare-duration night). Additive: a future sleep detail view can + * render the breakdown without changing the headline `latestValue`. + */ + sleepStages: Partial> | null; + /** + * Source-discrepancy annotation for the headline night's MAIN session, + * sleep tile only; `null` for every other kind and for a night whose + * writers agree. Additive (the established wire pattern for the + * iOS-locked shape): observational only; `latestValue` stays the winning + * writer's total; clients render a discreet "sources disagree" hint next + * to the number. Minutes are whole (rounded), matching the + * `/api/sleep/night` serialisation. + */ + sleepSourceDiscrepancy: SleepSourceDiscrepancy | null; + trend: "up" | "down" | "flat" | "unknown"; + sparkline: number[]; + updatedAt: string | null; + /** + * Total readings the user has ever logged for this metric, irrespective + * of the 7-day sparkline window. The dashboard tile keeps showing + * whenever `allTimeCount > 0` so a user with valid historical data isn't + * surprised by a tile disappearing during a logging gap. Distinct from + * `sparkline.length` which only carries the trailing-7-day points. + */ + allTimeCount: number; + /** + * ISO timestamp of the metric's single most recent reading. When + * `allTimeCount > 0` but the latest reading is older than 7 days, the iOS + * tile renders a muted "last reading N days ago" caption so the user + * understands the value isn't stale silently. `null` when the metric has + * no readings at all. + */ + lastSeenAt: string | null; + /** + * Which way a change of this metric counts for the person, against their + * own target or a profile default (`src/lib/targets/trend-verdict.ts`). + */ + direction?: TrendVerdict | null; + /** `direction` applied to this card's `trend`. */ + trendSentiment?: TrendSentiment; +} + +/** + * The verdict key behind each card: the measurement type it reads, or the + * derived key for the two cards that are not one. + */ +const VERDICT_KEY: Record = { + weight: "WEIGHT", + bloodPressure: "BLOOD_PRESSURE_SYS", + pulse: "PULSE", + bodyFat: "BODY_FAT", + glucose: "BLOOD_GLUCOSE", + sleep: "SLEEP_DURATION", + steps: "ACTIVITY_STEPS", + totalBodyWater: "TOTAL_BODY_WATER", + boneMass: "BONE_MASS", + oxygenSaturation: "OXYGEN_SATURATION", + mood: "MOOD", + bmi: "BMI", +}; + +/** + * Lay the verdict over each card. The reference placed against the band is + * the mean of the card's own seven-day sparkline, else its latest value, the + * seven-day average the web tiles place. Sleep cards are in hours and the + * verdict reads minutes. + */ +export function withVerdicts( + cards: MetricCard[], + ctx: Omit, +): MetricCard[] { + const referenceOf = (card: MetricCard | undefined): number | null => { + if (!card) return null; + const spark = card.sparkline.filter((v) => Number.isFinite(v)); + return spark.length > 0 + ? spark.reduce((sum, v) => sum + v, 0) / spark.length + : card.latestValue; + }; + // BMI is judged by the weight it is derived from (the target is a weight). + const weightReference = referenceOf(cards.find((c) => c.kind === "weight")); + return cards.map((card) => { + const reference = card.kind === "bmi" ? weightReference : referenceOf(card); + const direction = resolveTrendVerdict(VERDICT_KEY[card.kind], { + ...ctx, + referenceValue: + reference !== null && card.kind === "sleep" + ? reference * 60 + : reference, + }); + return { + ...card, + direction, + trendSentiment: trendSentimentOfTrend(card.trend, direction), + }; + }); +} + +/** + * Wire i18n keys, not translated strings. The legacy maps emitted German + * literals like "Gewicht" / "Schritte" which injected into an English iOS UI + * once the iOS app flipped its source language. Clients now resolve these + * keys against their own locale bundle (the PWA via `useTranslations()`, iOS + * via `Localizable.xcstrings`). The server payload becomes language-neutral + * and the response shrinks slightly. + * + * One key per `MetricKind`: symmetric maps so a new kind has to add both a + * title and a unit key (Type-checked: `Record`). + */ +const METRIC_TITLE_KEYS: Record = { + weight: "dashboard.metric.title.weight", + bloodPressure: "dashboard.metric.title.bloodPressure", + pulse: "dashboard.metric.title.pulse", + bodyFat: "dashboard.metric.title.bodyFat", + glucose: "dashboard.metric.title.glucose", + sleep: "dashboard.metric.title.sleep", + steps: "dashboard.metric.title.steps", + totalBodyWater: "dashboard.metric.title.totalBodyWater", + boneMass: "dashboard.metric.title.boneMass", + oxygenSaturation: "dashboard.metric.title.oxygenSaturation", + mood: "dashboard.metric.title.mood", + bmi: "dashboard.metric.title.bmi", +}; + +const METRIC_UNIT_KEYS: Record = { + weight: "dashboard.metric.unit.weight", + bloodPressure: "dashboard.metric.unit.bloodPressure", + pulse: "dashboard.metric.unit.pulse", + bodyFat: "dashboard.metric.unit.bodyFat", + glucose: "dashboard.metric.unit.glucose", + sleep: "dashboard.metric.unit.sleep", + steps: "dashboard.metric.unit.steps", + totalBodyWater: "dashboard.metric.unit.totalBodyWater", + boneMass: "dashboard.metric.unit.boneMass", + oxygenSaturation: "dashboard.metric.unit.oxygenSaturation", + mood: "dashboard.metric.unit.mood", + bmi: "dashboard.metric.unit.bmi", +}; + +/** + * The sleep sparkline: the trailing nights' TIME-ASLEEP in hours, + * reconstructed per night from the raw stage rows. The generic DAY-bucket + * rollup sparkline can't be used for sleep because it means across the + * per-stage rows (e.g. a 60-min DEEP row and a 240-min CORE row average to + * 150) rather than summing them into a night total. + */ +export function buildSleepSparkline( + rows: SleepStageRow[], + tz: string, + priorityJson: unknown, +): number[] { + return reconstructSleepNights(rows, tz, priorityJson) + .filter((n) => n.asleepMinutes > 0) + .map((n) => Math.round((n.asleepMinutes / 60) * 100) / 100) + .slice(-SPARK_DAYS); +} + +export function trendOf(values: number[]): MetricCard["trend"] { + if (values.length < 2) return "unknown"; + const first = values[0]; + const last = values[values.length - 1]; + const delta = last - first; + const epsilon = Math.max(1, Math.abs(first) * 0.01); + if (Math.abs(delta) < epsilon) return "flat"; + return delta > 0 ? "up" : "down"; +} + +/** A card with no sleep extras, from its reading and its spark. */ +export function plainCard( + id: string, + kind: MetricKind, + card: { + latestValue: number | null; + secondaryValue?: number | null; + spark: number[]; + updatedAt: string | null; + allTimeCount: number; + lastSeenAt: string | null; + }, +): MetricCard { + return { + id, + kind, + titleKey: METRIC_TITLE_KEYS[kind], + latestValue: card.latestValue, + secondaryValue: card.secondaryValue ?? null, + unitKey: METRIC_UNIT_KEYS[kind], + unit: null, + sleepStages: null, + sleepSourceDiscrepancy: null, + trend: trendOf(card.spark), + sparkline: card.spark, + updatedAt: card.updatedAt, + allTimeCount: card.allTimeCount, + lastSeenAt: card.lastSeenAt, + }; +} + +/** The sleep card: the latest night's TIME ASLEEP in hours. */ +export function sleepCard(card: { + latestValue: number | null; + sleepStages: Partial> | null; + sleepSourceDiscrepancy: SleepSourceDiscrepancy | null; + spark: number[]; + updatedAt: string | null; + allTimeCount: number; + lastSeenAt: string | null; +}): MetricCard { + return { + id: "sleep", + kind: "sleep", + titleKey: METRIC_TITLE_KEYS.sleep, + latestValue: card.latestValue, + secondaryValue: null, + unitKey: METRIC_UNIT_KEYS.sleep, + unit: "h", + sleepStages: card.sleepStages, + sleepSourceDiscrepancy: card.sleepSourceDiscrepancy, + trend: trendOf(card.spark), + sparkline: card.spark, + updatedAt: card.updatedAt, + allTimeCount: card.allTimeCount, + lastSeenAt: card.lastSeenAt, + }; +} + +/** The measurement types of the optional cards, in emit order. */ +export const OPTIONAL_CARD_TYPES = [ + ["BLOOD_GLUCOSE", "glucose"], + ["SLEEP_DURATION", "sleep"], + ["ACTIVITY_STEPS", "steps"], + ["TOTAL_BODY_WATER", "totalBodyWater"], + ["BONE_MASS", "boneMass"], + ["OXYGEN_SATURATION", "oxygenSaturation"], +] as const satisfies ReadonlyArray; diff --git a/src/lib/dashboard/summary/lookups.ts b/src/lib/dashboard/summary/lookups.ts new file mode 100644 index 000000000..9645065b1 --- /dev/null +++ b/src/lib/dashboard/summary/lookups.ts @@ -0,0 +1,86 @@ +/** + * Per-type lookups over the summary's bounded reads: the latest reading, + * the sparkline points and the all-time metadata of each measurement type. + */ +import type { MeasurementType } from "@/generated/prisma/client"; +import { CUMULATIVE_HK_TYPES } from "@/lib/measurements/apple-health-mapping"; +import type { + readAllTimeCounts, + readLatestEver, + readSparkBuckets, +} from "@/lib/dashboard/summary-reads"; + +export interface SummaryLookups { + latestOf(type: MeasurementType): { value: number; at: Date } | null; + sparkOf(type: MeasurementType): number[]; + metaForType(type: MeasurementType): { + allTimeCount: number; + lastSeenAt: string | null; + }; +} + +export function summaryLookups(reads: { + latestEver: Awaited>; + sparkBuckets: Awaited>; + allTimeAggregate: Awaited>; +}): SummaryLookups { + const { latestEver, sparkBuckets, allTimeAggregate } = reads; + // Per-type metadata lookup: a typed Map so a metric with no readings at + // all falls through `metaForType` to the `{ allTimeCount: 0, lastSeenAt: + // null }` default. + const allTimeByType = new Map< + MeasurementType, + { allTimeCount: number; lastSeenAt: Date | null } + >(); + // The newest reading is the one `latestEver` already read per type; its + // instant is the metric's last-seen time. + const lastSeenByType = new Map( + latestEver.map((row) => [row.type, row.measured_at]), + ); + for (const row of allTimeAggregate) { + allTimeByType.set(row.type, { + allTimeCount: row.count, + lastSeenAt: lastSeenByType.get(row.type) ?? null, + }); + } + + // Per-type latest (one row per type), carrying the most recent reading + // regardless of age: the iOS tile shows the historical value plus a muted + // "last reading N days ago" caption rather than nothing at all. + const latestByType = new Map(); + for (const row of latestEver) { + latestByType.set(row.type, { + value: Number(row.value), + at: new Date(row.measured_at), + }); + } + // Cumulative tiles paint the daily SUM; spot tiles keep the daily MEAN. + // Reading `sum_value` directly off the rollup row avoids a `mean * count` + // reconstruction; the fallback covers rollup rows written before the sum + // column existed, during the boot-backfill convergence window. + const sparkByType = new Map(); + for (const row of sparkBuckets) { + const list = sparkByType.get(row.type) ?? []; + const cumulativePoint = + row.sum_value !== null + ? Number(row.sum_value) + : Number(row.mean) * Number(row.count); + const point = CUMULATIVE_HK_TYPES.has(row.type) + ? cumulativePoint + : Number(row.mean); + list.push(point); + sparkByType.set(row.type, list); + } + + return { + latestOf: (type) => latestByType.get(type) ?? null, + sparkOf: (type) => sparkByType.get(type) ?? [], + metaForType: (type) => { + const slot = allTimeByType.get(type); + return { + allTimeCount: slot?.allTimeCount ?? 0, + lastSeenAt: slot?.lastSeenAt?.toISOString() ?? null, + }; + }, + }; +} diff --git a/src/lib/dashboard/summary/measurement-cards.ts b/src/lib/dashboard/summary/measurement-cards.ts new file mode 100644 index 000000000..8fe8e5103 --- /dev/null +++ b/src/lib/dashboard/summary/measurement-cards.ts @@ -0,0 +1,181 @@ +/** + * The measurement-backed cards of the dashboard summary, in emit order: + * weight, blood pressure, pulse, body fat, then the optional cards. Every + * card carries `allTimeCount` + `lastSeenAt` so the iOS tile renderer can + * keep a metric visible during a logging gap (gate on `allTimeCount > 0`) + * and paint a muted "last reading N days ago" caption when the most recent + * reading is older than the 7-day sparkline window. + */ +import type { SleepStage } from "@/generated/prisma/client"; +import type { + summarizeSleepNights, + SleepStageRow, +} from "@/lib/analytics/sleep-night"; + +import { + buildSleepSparkline, + OPTIONAL_CARD_TYPES, + plainCard, + sleepCard, + type MetricCard, +} from "./cards"; +import type { SummaryLookups } from "./lookups"; + +export interface SummarySleepInput { + summary: ReturnType; + rows: SleepStageRow[]; + tz: string; + priorityJson: unknown; +} + +/** + * Blood pressure is a paired metric: the tile is "alive" whenever either + * side of the pair has history, and an account that has never logged it + * gets no empty placeholder tile. The count sums both sides; the most + * recent last-seen instant of the two drives the staleness hint. + */ +function bloodPressureCard(lookups: SummaryLookups): MetricCard | null { + const latestSys = lookups.latestOf("BLOOD_PRESSURE_SYS"); + const latestDia = lookups.latestOf("BLOOD_PRESSURE_DIA"); + const sysSpark = lookups.sparkOf("BLOOD_PRESSURE_SYS"); + const sysMeta = lookups.metaForType("BLOOD_PRESSURE_SYS"); + const diaMeta = lookups.metaForType("BLOOD_PRESSURE_DIA"); + const bpAllTimeCount = sysMeta.allTimeCount + diaMeta.allTimeCount; + const bpLastSeenAt = ((): string | null => { + const sysAt = sysMeta.lastSeenAt; + const diaAt = diaMeta.lastSeenAt; + if (!sysAt) return diaAt; + if (!diaAt) return sysAt; + return sysAt >= diaAt ? sysAt : diaAt; + })(); + if (!latestSys && !latestDia && bpAllTimeCount === 0) return null; + return plainCard("bp", "bloodPressure", { + latestValue: latestSys?.value ?? null, + secondaryValue: latestDia?.value ?? null, + spark: sysSpark, + updatedAt: + latestSys?.at?.toISOString() ?? + latestDia?.at?.toISOString() ?? + bpLastSeenAt, + allTimeCount: bpAllTimeCount, + lastSeenAt: bpLastSeenAt, + }); +} + +/** + * Sleep is night-aggregated, not single-stage: the latest night's TIME + * ASLEEP (CORE/light + DEEP + REM, excluding IN_BED + AWAKE) in HOURS, the + * per-stage breakdown, and a sparkline of the trailing nights' asleep hours + * from the night reconstruction (the DAY-bucket rollup sparkline blends + * stages and would be misleading for sleep). + */ +function sleepNightCard( + sleep: SummarySleepInput, + meta: { allTimeCount: number; lastSeenAt: string | null }, +): MetricCard { + const night = sleep.summary.latestNight; + const toHours = (minutes: number): number => + Math.round((minutes / 60) * 100) / 100; + const nightSpark = buildSleepSparkline( + sleep.rows, + sleep.tz, + sleep.priorityJson, + ); + const stageHours: Partial> | null = night + ? Object.fromEntries( + Object.entries(night.stages).map(([s, m]) => [s, toHours(m)]), + ) + : null; + return sleepCard({ + latestValue: night ? toHours(night.asleepMinutes) : null, + sleepStages: + stageHours && Object.keys(stageHours).length > 0 ? stageHours : null, + // Additive, observational: the headline night's MAIN session saw two + // writer buckets with clearly different asleep totals. Rounded to whole + // minutes, matching the `/api/sleep/night` serialisation. + sleepSourceDiscrepancy: night?.sourceDiscrepancy + ? { + deltaMinutes: Math.round(night.sourceDiscrepancy.deltaMinutes), + sources: night.sourceDiscrepancy.sources.map((b) => ({ + source: b.source, + deviceType: b.deviceType, + asleepMinutes: Math.round(b.asleepMinutes), + })), + } + : null, + spark: nightSpark, + updatedAt: night?.measuredAt.toISOString() ?? meta.lastSeenAt, + allTimeCount: meta.allTimeCount, + lastSeenAt: night?.measuredAt.toISOString() ?? meta.lastSeenAt, + }); +} + +export function measurementCards( + lookups: SummaryLookups, + sleep: SummarySleepInput, +): MetricCard[] { + const metrics: MetricCard[] = []; + + // Weight is always emitted. + { + const latest = lookups.latestOf("WEIGHT"); + const meta = lookups.metaForType("WEIGHT"); + metrics.push( + plainCard("weight", "weight", { + latestValue: latest?.value ?? null, + spark: lookups.sparkOf("WEIGHT"), + updatedAt: latest?.at?.toISOString() ?? meta.lastSeenAt, + allTimeCount: meta.allTimeCount, + lastSeenAt: meta.lastSeenAt, + }), + ); + } + + const bp = bloodPressureCard(lookups); + if (bp) metrics.push(bp); + + // Pulse and body fat: emitted whenever there is a latest reading or any + // history, so a tile doesn't disappear during a fortnight without a fresh + // reading and an account that never logged one gets no placeholder. + for (const [type, id, kind] of [ + ["PULSE", "pulse", "pulse"], + ["BODY_FAT", "bodyFat", "bodyFat"], + ] as const) { + const latest = lookups.latestOf(type); + const meta = lookups.metaForType(type); + if (latest || meta.allTimeCount > 0) { + metrics.push( + plainCard(id, kind, { + latestValue: latest?.value ?? null, + spark: lookups.sparkOf(type), + updatedAt: latest?.at?.toISOString() ?? meta.lastSeenAt, + allTimeCount: meta.allTimeCount, + lastSeenAt: meta.lastSeenAt, + }), + ); + } + } + + // Optional cards: emitted whenever the user has *ever* logged the metric, + // so a glucose tile that hasn't been touched in 10 days still renders with + // the historical value visible (paired with the lastSeenAt caption). + for (const [type, kind] of OPTIONAL_CARD_TYPES) { + const latest = lookups.latestOf(type); + const meta = lookups.metaForType(type); + if (!latest && meta.allTimeCount === 0) continue; + if (kind === "sleep") { + metrics.push(sleepNightCard(sleep, meta)); + continue; + } + metrics.push( + plainCard(kind, kind, { + latestValue: latest?.value ?? null, + spark: lookups.sparkOf(type), + updatedAt: latest?.at.toISOString() ?? meta.lastSeenAt, + allTimeCount: meta.allTimeCount, + lastSeenAt: meta.lastSeenAt, + }), + ); + } + return metrics; +} diff --git a/src/lib/documents/upload-answer.ts b/src/lib/documents/upload-answer.ts new file mode 100644 index 000000000..9e7276b08 --- /dev/null +++ b/src/lib/documents/upload-answer.ts @@ -0,0 +1,66 @@ +/** + * The stored row a vault upload answers with (`POST /api/documents/inbound`), + * serialised with its condition and visit links: a fresh insert with no facts + * yet, or the existing live row a duplicate resolved to, with its fact + * tallies. + */ +import { prisma } from "@/lib/db"; +import { + loadConditionLinks, + loadDocumentEncounterLinks, +} from "@/lib/documents/links"; +import { + serialiseDocument, + type SerialisableDocument, +} from "@/lib/documents/store"; + +/** The existing row a duplicate upload resolved to. */ +export async function duplicateDocumentBody( + userId: string, + existing: SerialisableDocument, +) { + const [links, visitLinks, groups] = await Promise.all([ + loadConditionLinks(userId, [existing.id]), + loadDocumentEncounterLinks(userId, [existing.id]), + prisma.extractedFact.groupBy({ + by: ["status"], + where: { userId, documentId: existing.id }, + _count: { _all: true }, + }), + ]); + let factCount = 0; + let pendingCount = 0; + for (const g of groups) { + if (g.status !== "REJECTED") factCount += g._count._all; + if (g.status === "PENDING") pendingCount += g._count._all; + } + return serialiseDocument( + existing, + { factCount, pendingCount }, + links.get(existing.id) ?? [], + false, + null, + false, + visitLinks.get(existing.id) ?? [], + ); +} + +/** A row the upload just stored. */ +export async function storedDocumentBody( + userId: string, + document: SerialisableDocument, +) { + const [links, visitLinks] = await Promise.all([ + loadConditionLinks(userId, [document.id]), + loadDocumentEncounterLinks(userId, [document.id]), + ]); + return serialiseDocument( + document, + { factCount: 0, pendingCount: 0 }, + links.get(document.id) ?? [], + false, + null, + false, + visitLinks.get(document.id) ?? [], + ); +} diff --git a/src/lib/documents/upload-request.ts b/src/lib/documents/upload-request.ts new file mode 100644 index 000000000..fdf00758d --- /dev/null +++ b/src/lib/documents/upload-request.ts @@ -0,0 +1,142 @@ +/** + * Reading one vault upload (`POST /api/documents/inbound`): the source key + * that may ride the query string, the bounded multipart body, the metadata + * fields beside the file, and the one source key the upload is filed under. + * Each step answers what it found; the route turns a refusal into its + * response. + */ +import type { z } from "zod/v4"; + +import { BodyTooLargeError, readBoundedBody } from "@/lib/labs/ocr-upload"; +import { + documentCreateSchema, + documentSourceKeySchema, + type DocumentSourceSystemValue, +} from "@/lib/validations/inbound-documents"; + +const UPLOAD_BODY_TIMEOUT_MS = 30_000; + +export interface QuerySourceKey { + sourceSystem: DocumentSourceSystemValue; + sourceId: string; + sourceInstance?: string; +} + +/** + * The source key in the query string. Both halves or neither; half a key is + * a mistake worth saying so about. + */ +export function parseQuerySourceKey( + url: URL, +): + | { kind: "none" } + | { kind: "invalid"; issues: z.core.$ZodIssue[] } + | { kind: "key"; key: QuerySourceKey } { + const querySystem = url.searchParams.get("sourceSystem"); + const queryId = url.searchParams.get("sourceId"); + const queryInstance = url.searchParams.get("sourceInstance"); + if (querySystem === null && queryId === null) return { kind: "none" }; + const parsedKey = documentSourceKeySchema.safeParse({ + sourceSystem: querySystem ?? undefined, + sourceId: queryId ?? undefined, + sourceInstance: queryInstance ?? undefined, + }); + if (!parsedKey.success) { + return { kind: "invalid", issues: parsedKey.error.issues }; + } + return { kind: "key", key: parsedKey.data }; +} + +/** + * Read the multipart body, aborting at `bodyCap` bytes or after the upload + * deadline, and parse it as form data. + */ +export async function readUploadForm( + request: Request, + bodyCap: number, +): Promise< + | { kind: "form"; formData: FormData } + | { kind: "tooLarge" } + | { kind: "timeout" } + | { kind: "invalid" } +> { + try { + const bytes = await readBoundedBody(request.body, bodyCap, { + signal: request.signal, + deadline: Date.now() + UPLOAD_BODY_TIMEOUT_MS, + }); + const formData = await new Request(request.url, { + method: "POST", + headers: { "content-type": request.headers.get("content-type") ?? "" }, + body: bytes, + }).formData(); + return { kind: "form", formData }; + } catch (err) { + if (err instanceof BodyTooLargeError) return { kind: "tooLarge" }; + if (err instanceof DOMException && err.name === "AbortError") { + return { kind: "timeout" }; + } + return { kind: "invalid" }; + } +} + +/** + * The optional metadata (title / kind / documentDate / episodeIds / + * encounterIds / source key / aiRead): the form fields beside the file. Both + * id lists may be repeated. + */ +export function parseUploadMetadata(formData: FormData) { + const rawEpisodeIds = formData + .getAll("episodeIds") + .filter((v): v is string => typeof v === "string"); + const rawEncounterIds = formData + .getAll("encounterIds") + .filter((v): v is string => typeof v === "string"); + return documentCreateSchema.safeParse({ + title: formData.get("title") ?? undefined, + kind: formData.get("kind") ?? undefined, + documentDate: formData.get("documentDate") ?? undefined, + episodeIds: rawEpisodeIds.length > 0 ? rawEpisodeIds : undefined, + encounterIds: rawEncounterIds.length > 0 ? rawEncounterIds : undefined, + sourceSystem: formData.get("sourceSystem") ?? undefined, + sourceId: formData.get("sourceId") ?? undefined, + sourceInstance: formData.get("sourceInstance") ?? undefined, + aiRead: formData.get("aiRead") ?? undefined, + }); +} + +/** + * The key from the query string, or from the form fields; if a caller sends + * both they have to agree. + */ +export function resolveUploadSourceKey( + queryKey: QuerySourceKey | null, + metadata: z.infer, +): + | { kind: "mismatch" } + | { + kind: "key"; + sourceSystem: DocumentSourceSystemValue | null; + sourceId: string | null; + sourceInstance: string | null; + } { + const formSystem = metadata.sourceSystem ?? null; + const formId = formSystem ? (metadata.sourceId ?? null) : null; + const formInstance = formId ? (metadata.sourceInstance ?? null) : null; + if ( + queryKey && + formSystem !== null && + (formSystem !== queryKey.sourceSystem || + formId !== queryKey.sourceId || + (formInstance !== null && + formInstance !== (queryKey.sourceInstance ?? null))) + ) { + return { kind: "mismatch" }; + } + const sourceSystem = queryKey?.sourceSystem ?? formSystem; + const sourceId = queryKey?.sourceId ?? formId; + const sourceInstance = sourceId + ? (queryKey?.sourceInstance ?? formInstance ?? null) + : null; + return { kind: "key", sourceSystem, sourceId, sourceInstance }; +} diff --git a/src/lib/documents/upload-responses.ts b/src/lib/documents/upload-responses.ts new file mode 100644 index 000000000..0dcaba140 --- /dev/null +++ b/src/lib/documents/upload-responses.ts @@ -0,0 +1,144 @@ +/** + * The answers a vault upload (`POST /api/documents/inbound`) gives besides a + * freshly stored row: the receipt a narrow token gets, a duplicate, a + * re-send of a deleted import, the alias limit, and the size refusal. + */ +import { NextResponse } from "next/server"; + +import { apiError } from "@/lib/api-response"; +import { annotate } from "@/lib/logging/context"; +import type { SerialisableDocument } from "@/lib/documents/store"; +import type { SourceKeyMatch } from "@/lib/documents/source-key"; +import { duplicateDocumentBody } from "@/lib/documents/upload-answer"; +import type { DocumentSourceSystemValue } from "@/lib/validations/inbound-documents"; + +/** §3.2 — 413 fileTooLarge with the configured limit in `meta`. */ +export function fileTooLarge(maxFileBytes: number): NextResponse { + return apiError("File is too large.", 413, { + errorCode: "documents.inbound.fileTooLarge", + reason: "fileTooLarge", + maxFileBytes, + }); +} + +/** What a single upload asked for, carried into the annotations. */ +export interface UploadContext { + userId: string; + /** A narrow `documents:write` token, not a session or a `["*"]` token. */ + scoped: boolean; + sourceSystem: DocumentSourceSystemValue | null; + aiDeferred: boolean; +} + +/** + * The receipt a scoped caller gets in place of the stored row: the id and + * whether the upload was a duplicate. A write-only credential holder who + * knows a file's bytes learns that it is stored, not its title, filename, + * links or fact counts. + */ +export function receiptResponse( + id: string | null, + status: 200 | 201, + flags: { duplicate: boolean; deleted?: boolean }, +): NextResponse { + const data = { + id, + duplicate: flags.duplicate, + ...(flags.deleted ? { deleted: true } : {}), + }; + return NextResponse.json( + { + data, + error: null, + ...(flags.duplicate + ? { + meta: { + duplicate: true, + ...(flags.deleted ? { deleted: true } : {}), + }, + } + : {}), + }, + { status }, + ); +} + +/** + * A re-send of an imported document the person deleted. + * 200 and no new row, for every caller: the deletion was a decision, and an + * importer re-running over the same source must not undo it. `id` is the + * tombstoned row while it exists and null once the purge took it; there is no + * row left to serialise either way, so the answer is the receipt. + */ +export function deletedResponse( + ctx: UploadContext, + id: string | null, +): NextResponse { + annotate({ + action: { name: "documents.vault.upload" }, + meta: { + documentId: id, + duplicate: true, + deleted: true, + scoped: ctx.scoped, + sourceSystem: ctx.sourceSystem, + aiDeferred: ctx.aiDeferred, + }, + }); + return receiptResponse(id, 200, { duplicate: true, deleted: true }); +} + +/** + * The same bytes have already been sent under too many source keys + * (`MAX_SOURCE_ALIASES_PER_DOCUMENT`). A conflict with what is stored, not a + * rate: nothing is remembered and nothing stored. + */ +export function aliasLimitResponse(): NextResponse { + return apiError( + "This file is already stored under too many source ids.", + 409, + { errorCode: "documents.inbound.sourceAliasLimit" }, + ); +} + +/** Answer a source key that is already held: duplicate, or deleted. */ +export function answerSourceKey( + ctx: UploadContext, + match: SourceKeyMatch, +): Promise | NextResponse { + return match.state === "live" + ? duplicateResponse(ctx, match.document) + : deletedResponse(ctx, match.id); +} + +/** + * §3.2 — a duplicate upload is NOT an error: return the existing live row + * with `meta.duplicate: true` at the envelope level (the UI toasts "already + * stored" and highlights the row). A scoped caller gets the receipt instead. + */ +export async function duplicateResponse( + ctx: UploadContext, + existing: SerialisableDocument, +): Promise { + annotate({ + action: { name: "documents.vault.upload" }, + meta: { + documentId: existing.id, + duplicate: true, + scoped: ctx.scoped, + sourceSystem: ctx.sourceSystem, + aiDeferred: ctx.aiDeferred, + }, + }); + if (ctx.scoped) { + return receiptResponse(existing.id, 200, { duplicate: true }); + } + return NextResponse.json( + { + data: await duplicateDocumentBody(ctx.userId, existing), + error: null, + meta: { duplicate: true }, + }, + { status: 200 }, + ); +} diff --git a/src/lib/documents/vault-list.ts b/src/lib/documents/vault-list.ts new file mode 100644 index 000000000..d1c813ba7 --- /dev/null +++ b/src/lib/documents/vault-list.ts @@ -0,0 +1,209 @@ +/** + * The vault list read behind `GET /api/documents/inbound`: title/filename + * search unioned with a whole-word content match, kind / episode / visit / + * year / date-range filters, sort, and keyset pagination. It NEVER selects + * the encrypted blob column. + */ +import type { z } from "zod/v4"; + +import { prisma } from "@/lib/db"; +import type { Prisma } from "@/generated/prisma/client"; +import { listPage } from "@/lib/http/list-query"; +import { annotate } from "@/lib/logging/context"; +import { hashQueryTokens } from "@/lib/documents/content-index"; +import { isoDateToUtc } from "@/lib/documents/ingest"; +import { + loadConditionLinks, + loadDocumentEncounterLinks, +} from "@/lib/documents/links"; +import { serialiseDocument } from "@/lib/documents/store"; +import { + toContentIndexSource, + type documentListQuerySchema, +} from "@/lib/validations/inbound-documents"; + +type VaultListQuery = z.infer; + +function listWhere( + userId: string, + query: VaultListQuery, +): Prisma.InboundDocumentWhereInput { + const { q, kind, episodeId, encounterId, year, from, to } = query; + const where: Prisma.InboundDocumentWhereInput = { + userId, + deletedAt: null, + }; + if (kind && kind.length > 0) where.kind = { in: kind }; + if (episodeId) where.conditionLinks = { some: { episodeId } }; + // The visit filter reads the SAME table the visit's own sheet writes, from + // the document side. Filtering rather than post-filtering: a page of fifty + // documents narrowed in Node would page wrongly. + if (encounterId) where.encounterLinks = { some: { encounterId } }; + if (year !== undefined) { + where.documentDate = { + gte: new Date(Date.UTC(year, 0, 1)), + lt: new Date(Date.UTC(year + 1, 0, 1)), + }; + } else if (from || to) { + where.documentDate = { + ...(from ? { gte: isoDateToUtc(from) } : {}), + // inclusive end-of-day for the `to` bound + ...(to ? { lt: new Date(isoDateToUtc(to).getTime() + 86_400_000) } : {}), + }; + } + if (q) { + // Substring match on the short plaintext fields … + const or: Prisma.InboundDocumentWhereInput[] = [ + { title: { contains: q, mode: "insensitive" } }, + { filename: { contains: q, mode: "insensitive" } }, + ]; + // … unioned with a WHOLE-WORD content match over the blind token index. + // The query is tokenised + HMAC'd the same way the index was built, then + // matched with a GIN-accelerated array-overlap (`hasSome` → `&&`). The + // list still never selects the encrypted text: only the opaque hashes + // are touched, in the related table. Degrades silently to title/filename + // when the caller has no indexed documents (no rows overlap). + const hashes = hashQueryTokens(q); + if (hashes.length > 0) { + or.push({ contentIndex: { is: { searchTokens: { hasSome: hashes } } } }); + } + where.OR = or; + } + return where; +} + +/** + * The sort column of a page; nullable sort columns sort nulls last so + * undated documents trail. + */ +function primaryOrder( + query: VaultListQuery, +): Prisma.InboundDocumentOrderByWithRelationInput { + const { sort, order } = query; + return sort === "documentDate" + ? { documentDate: { sort: order, nulls: "last" } } + : sort === "title" + ? { title: { sort: order, nulls: "last" } } + : { createdAt: order }; +} + +/** + * Fact tallies for the whole page in ONE grouped count: no per-document + * fan-out and no materialising fact-id rows. `factCount` counts every + * non-REJECTED fact (a rejected fact is discarded, not part of the + * document's tally), `pendingCount` the PENDING subset awaiting review. + */ +async function pageFactCounts( + userId: string, + pageIds: string[], +): Promise> { + const factCounts = new Map< + string, + { factCount: number; pendingCount: number } + >(); + if (pageIds.length === 0) return factCounts; + const groups = await prisma.extractedFact.groupBy({ + by: ["documentId", "status"], + where: { userId, documentId: { in: pageIds } }, + _count: { _all: true }, + }); + for (const g of groups) { + const entry = factCounts.get(g.documentId) ?? { + factCount: 0, + pendingCount: 0, + }; + const n = g._count._all; + if (g.status !== "REJECTED") entry.factCount += n; + if (g.status === "PENDING") entry.pendingCount += n; + factCounts.set(g.documentId, entry); + } + return factCounts; +} + +export async function listVaultDocuments( + userId: string, + query: VaultListQuery, +) { + const { q, kind, episodeId, encounterId, year, sort, order, cursor } = query; + const where = listWhere(userId, query); + // Keyset pagination on the sort column + a stable `id` tiebreak. + const orderBy: Prisma.InboundDocumentOrderByWithRelationInput[] = [ + primaryOrder(query), + { id: order }, + ]; + + // Hardening: the list NEVER fetches the encrypted blob column; a page of + // 50 rows would otherwise drag up to 50 × cap ciphertext bytes per request. + const { rows: page, cap } = await listPage(query.limit, (take) => + prisma.inboundDocument.findMany({ + where, + omit: { contentEncrypted: true }, + orderBy, + take, + ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}), + }), + ); + + const hasMore = cap.truncated; + const nextCursor = hasMore ? (page[page.length - 1]?.id ?? null) : null; + + const pageIds = page.map((d) => d.id); + const factCounts = await pageFactCounts(userId, pageIds); + + // Condition/visit links + the content-index and thumbnail probes for the + // page, all in ONE Promise.all: four independent grouped queries (no N+1, + // never a ciphertext/blob column), and the vault polls this list every few + // seconds so two serialized round-trips were pure added latency. + const [linkMap, visitLinkMap, indexedRows, thumbRows] = await Promise.all([ + loadConditionLinks(userId, pageIds), + loadDocumentEncounterLinks(userId, pageIds), + // Which of the page's documents have a content index (drives the + // searchable status + the provenance the UI reads to tell an AI-read + // document from a locally-indexed one). + page.length > 0 + ? prisma.documentContentIndex.findMany({ + where: { userId, documentId: { in: pageIds } }, + select: { documentId: true, source: true }, + }) + : Promise.resolve([]), + // Which of the page's documents have a preview thumbnail (gates the + // card's ). Grouped on the 1:1 side table. + page.length > 0 + ? prisma.documentThumbnail.findMany({ + where: { userId, documentId: { in: pageIds } }, + select: { documentId: true }, + }) + : Promise.resolve([]), + ]); + const indexSources = new Map(); + for (const row of indexedRows) indexSources.set(row.documentId, row.source); + const thumbnailIds = new Set(); + for (const row of thumbRows) thumbnailIds.add(row.documentId); + + annotate({ + action: { name: "documents.inbound.list" }, + meta: { + count: page.length, + sort, + order, + filtered: Boolean( + q || (kind && kind.length > 0) || episodeId || encounterId || year, + ), + }, + }); + + return { + documents: page.map((doc) => + serialiseDocument( + doc, + factCounts.get(doc.id) ?? { factCount: 0, pendingCount: 0 }, + linkMap.get(doc.id) ?? [], + indexSources.has(doc.id), + toContentIndexSource(indexSources.get(doc.id)), + thumbnailIds.has(doc.id), + visitLinkMap.get(doc.id) ?? [], + ), + ), + nextCursor, + }; +} diff --git a/src/lib/import/csv-write.ts b/src/lib/import/csv-write.ts new file mode 100644 index 000000000..c72fba6dd --- /dev/null +++ b/src/lib/import/csv-write.ts @@ -0,0 +1,500 @@ +/** + * The batched writer behind `POST /api/import/csv`. + * + * The route reads, parses and bounds the file; this module writes its + * accepted rows. It pre-fetches the existing rows under the IMPORT source in + * one query per dedup-key shape (per type and chunk), partitions the file + * into insert and update sets, then writes them with `createManyAndReturn` + * and grouped `updateMany` in bounded chunks. The per-line + * inserted / updated / duplicate envelope is reconstructed from the + * partition. Serial per-row upserts once approached the request timeout on a + * few-thousand-row file; this is the shape `/measurements/batch` uses. + */ +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import { auditLog } from "@/lib/auth/audit"; +import { encryptNote, readNote } from "@/lib/crypto/note-cipher"; +import type { CsvRowResult } from "@/lib/import/csv-measurements"; +import type { NormalisedMeasurementRow } from "@/lib/import/csv-measurements"; +import { afterMeasurementMutation } from "@/lib/rollups/after-measurement-mutation"; +import type { + GlucoseContext, + MeasurementType, + Prisma, +} from "@/generated/prisma/client"; +import { emitInsertedMeasurementArrivals } from "@/lib/arrivals/measurement-emit"; +import { maybeEnqueueMorningRefresh } from "@/lib/daily/morning-refresh-trigger"; + +/** Keys per dedup probe: well under Postgres's bind-parameter cap. */ +const LOOKUP_CHUNK = 1000; +/** Rows per create call / per update batch transaction. */ +const WRITE_CHUNK = 200; + +/** A parse-`ok` line with its normalised row. */ +export type CsvOkRow = CsvRowResult & { + row: NonNullable; +}; + +type WriteOutcome = "inserted" | "updated" | "duplicate"; + +interface StoredImportRow { + value: number; + unit: string; + measuredAt: Date; + glucoseContext: GlucoseContext | null; + notes: string | null; + notesEncrypted: Uint8Array | null; + deletedAt: Date | null; +} + +/** The counts and per-line outcomes of one import, filled as it plans. */ +interface CsvTally { + inserted: number; + updated: number; + skipped: number; + /** + * Per-line write outcome for rows that reached the DB. Keyed by source + * line so the response envelope can surface `inserted` / `updated` / + * `duplicate` distinctly without polluting the parse-time `CsvRowReason`. + */ + outcome: Map; + touched: Array<{ type: MeasurementType; measuredAt: Date }>; +} + +function chunked(items: readonly T[], size: number): T[][] { + const out: T[][] = []; + for (let i = 0; i < items.length; i += size) { + out.push(items.slice(i, i + size)); + } + return out; +} + +/** Group rows' probe keys by measurement type, de-duplicated. */ +function groupByType( + rows: ReadonlyArray<{ row: NormalisedMeasurementRow }>, + keyOf: (r: { row: NormalisedMeasurementRow }) => K, +): Map { + const byType = new Map>(); + for (const r of rows) { + const type = r.row.type as MeasurementType; + const key = keyOf(r); + const bucket = byType.get(type) ?? new Map(); + bucket.set(key instanceof Date ? String(key.getTime()) : String(key), key); + byType.set(type, bucket); + } + return new Map([...byType].map(([type, keys]) => [type, [...keys.values()]])); +} + +/** + * True when a stored IMPORT row already holds exactly what this line would + * write: same value, unit, instant, glucose context and note, and live. Such + * a line is a duplicate, not an update. + */ +function sameStoredRow( + stored: StoredImportRow, + m: NormalisedMeasurementRow, +): boolean { + return ( + stored.deletedAt === null && + stored.value === m.value && + stored.unit === m.unit && + stored.measuredAt.getTime() === m.measuredAt.getTime() && + stored.glucoseContext === + ((m.glucoseContext as GlucoseContext | undefined) ?? null) && + readNote(stored.notesEncrypted, stored.notes) === (m.notes || null) + ); +} + +/** + * Rows with an external id: idempotent upsert on + * `(userId, type, source=IMPORT, externalId)`. One probe per type and chunk + * fetches every pre-existing key with the columns an update would write, so + * a row whose stored values already match is left alone. + * + * Partitioned in file order, honouring in-file repeats: the first sighting + * of a brand-new key inserts, every later sighting (or any sighting of a key + * already in the DB) updates in place. The last value for a key wins, + * matching the old sequential-upsert overwrite. Insert / update maps are + * keyed by (type, externalId) so a repeated key collapses to one write. A + * key already in the DB whose stored row equals this line is a duplicate: no + * write, reported like the natural-key duplicates. That is what makes + * re-importing the same file cheap: a second upload of an unchanged + * 9 000-row export writes nothing. + */ +async function planExternalIdRows( + userId: string, + extRows: CsvOkRow[], + tally: CsvTally, +): Promise<{ + inserts: Map; + updates: Map; +}> { + const existing = new Map(); + for (const [type, ids] of groupByType( + extRows, + (r) => r.row.externalId as string, + )) { + for (const chunk of chunked(ids, LOOKUP_CHUNK)) { + const found = await prisma.measurement.findMany({ + where: { + userId, + source: "IMPORT", + type, + externalId: { in: chunk }, + }, + select: { + externalId: true, + value: true, + unit: true, + measuredAt: true, + glucoseContext: true, + notes: true, + notesEncrypted: true, + deletedAt: true, + }, + }); + for (const row of found) { + existing.set(`${type}::${row.externalId}`, row); + } + } + } + + const seen = new Set(); + const inserts = new Map(); + const updates = new Map(); + for (const result of extRows) { + const m = result.row; + const key = `${m.type}::${m.externalId}`; + const stored = existing.get(key); + if (stored && !updates.has(key) && sameStoredRow(stored, m)) { + tally.skipped++; + tally.outcome.set(result.line, "duplicate"); + continue; + } + if (stored) { + updates.set(key, m); + tally.updated++; + tally.outcome.set(result.line, "updated"); + } else if (seen.has(key)) { + // New-to-DB key seen again in this file: still a single create with + // the latest value, but this line reads as an update just as the + // sequential upsert did. + inserts.set(key, m); + tally.updated++; + tally.outcome.set(result.line, "updated"); + } else { + seen.add(key); + inserts.set(key, m); + tally.inserted++; + tally.outcome.set(result.line, "inserted"); + } + tally.touched.push({ + type: m.type as MeasurementType, + measuredAt: m.measuredAt, + }); + // An update that moves the reading to another instant leaves the day it + // came from: that day is re-folded too, or the old day keeps a reading + // that no longer exists there. + if (stored && stored.measuredAt.getTime() !== m.measuredAt.getTime()) { + tally.touched.push({ + type: m.type as MeasurementType, + measuredAt: stored.measuredAt, + }); + } + } + return { inserts, updates }; +} + +/** + * Rows without an external id: create-only, deduped on the natural unique + * key (userId, type, measuredAt, source=IMPORT, sleepStage=null). A + * collision, against the DB or an earlier row in this same file, is a + * duplicate, never an overwrite (each sample is a canonical reading). + * Probed per type and chunk, like the external-id rows. + */ +async function planPlainRows( + userId: string, + plainRows: CsvOkRow[], + tally: CsvTally, +): Promise { + const existing = new Set(); + for (const [type, instants] of groupByType( + plainRows, + (r) => r.row.measuredAt, + )) { + for (const chunk of chunked(instants, LOOKUP_CHUNK)) { + const found = await prisma.measurement.findMany({ + where: { + userId, + source: "IMPORT", + type, + measuredAt: { in: chunk }, + }, + select: { measuredAt: true }, + }); + for (const row of found) { + existing.add(`${type}::${row.measuredAt.getTime()}`); + } + } + } + const seen = new Set(); + const inserts: NormalisedMeasurementRow[] = []; + for (const result of plainRows) { + const m = result.row; + const key = `${m.type}::${m.measuredAt.getTime()}`; + if (existing.has(key) || seen.has(key)) { + tally.skipped++; + tally.outcome.set(result.line, "duplicate"); + } else { + seen.add(key); + inserts.push(m); + tally.inserted++; + tally.outcome.set(result.line, "inserted"); + tally.touched.push({ + type: m.type as MeasurementType, + measuredAt: m.measuredAt, + }); + } + } + return inserts; +} + +/** + * Under `skipDuplicates` a conflicting row is silently absorbed: the key is + * present in the table either way, so the SPECIFIC raced lines cannot be + * identified; only the counters and per-line statuses have to sum to the + * truth. Downgrade enough `inserted` lines to `duplicate` so the envelope + * matches the DB write count (the batch route's raced-duplicate downgrade). + */ +function downgradeRacedInserts(tally: CsvTally, raced: number): void { + if (raced <= 0) return; + let downgraded = 0; + for (const [line, outcome] of tally.outcome) { + if (downgraded >= raced) break; + if (outcome === "inserted") { + tally.outcome.set(line, "duplicate"); + tally.inserted--; + tally.skipped++; + downgraded++; + } + } +} + +/** + * Bulk write in bounded chunks, each its own short transaction. + * + * This used to be ONE interactive transaction around every create chunk AND + * one `updateMany` per re-imported row. Re-importing a 9 000-row file with an + * externalId column meant 9 000 serial updates inside it, which outlived + * Prisma's 5 s interactive-transaction timeout and ended in a 500. A chunk + * now commits on its own: creates go through `createManyAndReturn` + * (skipDuplicates absorbs a concurrent double-submit), updates through one + * batch transaction per chunk. A failure part-way leaves the earlier chunks + * written, and that is safe to retry: a re-upload of the same file skips + * what landed (unchanged externalId rows and natural-key duplicates are not + * written again). + */ +async function writeRows( + userId: string, + toCreate: Prisma.MeasurementCreateManyInput[], + updates: NormalisedMeasurementRow[], + tally: CsvTally, +): Promise { + let createdCount = 0; + const createdRows: Array<{ + id: string; + type: MeasurementType; + measuredAt: Date; + }> = []; + for (const chunk of chunked(toCreate, WRITE_CHUNK)) { + const rows = await prisma.measurement.createManyAndReturn({ + data: chunk, + skipDuplicates: true, + select: { id: true, type: true, measuredAt: true }, + }); + createdRows.push(...rows); + createdCount += rows.length; + } + for (const chunk of chunked(updates, WRITE_CHUNK)) { + await prisma.$transaction( + chunk.map((m) => + prisma.measurement.updateMany({ + where: { + userId, + source: "IMPORT", + type: m.type as MeasurementType, + externalId: m.externalId as string, + }, + data: { + value: m.value, + unit: m.unit, + measuredAt: m.measuredAt, + notes: null, + notesEncrypted: encryptNote(m.notes ?? null), + glucoseContext: + (m.glucoseContext as GlucoseContext | undefined) ?? null, + // No-op on a live row, a deliberate RESURRECTION on a tombstoned + // one: IMPORT rows are re-importable by design, so a re-imported + // externalId brings the row back (mirrors the source-owned sync + // resurrect rule). + deletedAt: null, + }, + }), + ), + ); + } + + void emitInsertedMeasurementArrivals(userId, createdRows, "csv_import").catch( + () => {}, + ); + void maybeEnqueueMorningRefresh( + userId, + createdRows + .filter((row) => row.type === "SLEEP_DURATION") + .map((row) => row.measuredAt), + ).catch(() => {}); + + downgradeRacedInserts(tally, toCreate.length - createdCount); +} + +async function importRows( + userId: string, + okRows: CsvOkRow[], + tally: CsvTally, + ipAddress: string | null, +): Promise { + const buildCreateData = ( + m: NormalisedMeasurementRow, + ): Prisma.MeasurementCreateManyInput => ({ + userId, + type: m.type as MeasurementType, + value: m.value, + unit: m.unit, + source: "IMPORT", + ...(m.externalId ? { externalId: m.externalId } : {}), + measuredAt: m.measuredAt, + notes: null, + notesEncrypted: encryptNote(m.notes ?? null), + glucoseContext: (m.glucoseContext as GlucoseContext | undefined) ?? null, + }); + + const ext = await planExternalIdRows( + userId, + okRows.filter((r) => r.row.externalId), + tally, + ); + const plainInserts = await planPlainRows( + userId, + okRows.filter((r) => !r.row.externalId), + tally, + ); + + const toCreate: Prisma.MeasurementCreateManyInput[] = [ + ...[...ext.inserts.values()].map(buildCreateData), + ...plainInserts.map(buildCreateData), + ]; + // The re-fold runs whatever happens to the writes. A chunk that fails + // part-way leaves the earlier chunks written, and a retry reads those rows + // as duplicates and touches nothing: without the re-fold here their days + // would keep the rollups from before the import for good. Re-folding a + // (type, day) nothing landed on is harmless. + try { + await writeRows(userId, toCreate, [...ext.updates.values()], tally); + } finally { + // One bounded rollup re-fold per touched (type, day): a 10 000-row CSV + // pays at most ~N (type, day) recomputes, not 10 000 per-row hooks, and + // the status re-warm for the touched types. Best-effort: a populator + // hiccup never fails the importer. + await afterMeasurementMutation(userId, tally.touched, { + source: "manual", + label: "import.csv", + }); + } + + await auditLog("import.csv.upload", { + userId, + ipAddress, + details: { + inserted: tally.inserted, + updated: tally.updated, + skipped: tally.skipped, + }, + }); +} + +/** + * Write (or, with `dryRun`, only project) one parsed CSV file and answer the + * route's envelope: `{ inserted, updated, skipped, total, dryRun, rows }`. + */ +export async function runCsvImport(input: { + userId: string; + rows: CsvRowResult[]; + okRows: CsvOkRow[]; + dryRun: boolean; + ipAddress: string | null; +}) { + const { rows, okRows, dryRun } = input; + const tally: CsvTally = { + inserted: 0, + updated: 0, + skipped: rows.filter((r) => r.status === "skipped").length, + outcome: new Map(), + touched: [], + }; + + if (!dryRun) { + await importRows(input.userId, okRows, tally, input.ipAddress); + } else { + // Dry-run preview: no write ran, so report the projected outcome: every + // valid parse-`ok` row would insert (an externalId row could land as an + // update, but that needs a DB read deliberately skipped here). + tally.inserted = okRows.length; + } + + const summary = { + inserted: tally.inserted, + updated: tally.updated, + skipped: tally.skipped, + total: rows.length, + dryRun, + // Per-row status, mirroring the batch route's per-entry envelope. The + // normalised payload is stripped: the client only needs line + status. + // Parse-time `ok` rows are refined by the write outcome: a row that hit + // a unique-constraint duplicate becomes `skipped`/`duplicate`; an upsert + // that landed on an existing row becomes `updated`. In `dryRun` mode no + // write ran, so every parse-`ok` row stays `inserted` (the projected + // outcome). + rows: rows.map((r) => { + if (r.status === "skipped") { + return { + line: r.line, + status: "skipped" as const, + ...(r.reason ? { reason: r.reason } : {}), + }; + } + const outcome = tally.outcome.get(r.line); + if (outcome === "duplicate") { + return { + line: r.line, + status: "skipped" as const, + reason: "duplicate", + }; + } + if (outcome === "updated") { + return { line: r.line, status: "updated" as const }; + } + return { line: r.line, status: "inserted" as const }; + }), + }; + + annotate({ + meta: { + import_csv_inserted: tally.inserted, + import_csv_updated: tally.updated, + import_csv_skipped: tally.skipped, + import_csv_dry_run: dryRun, + }, + }); + + return summary; +} diff --git a/src/lib/import/json/measurements.ts b/src/lib/import/json/measurements.ts new file mode 100644 index 000000000..1e57608ea --- /dev/null +++ b/src/lib/import/json/measurements.ts @@ -0,0 +1,161 @@ +/** + * The measurement rows of a JSON import (`POST /api/import`). + * + * The route's schema has already bounded each row (plausible range, entry + * instant). With an external id a row is inserted or, when the key is taken, + * updated in place, so a re-import of the same export is idempotent; without + * one it is a first-write-wins create with a NULL external id, which does + * duplicate on re-import. + */ +import type { z } from "zod/v4"; + +import { prisma } from "@/lib/db"; +import type { MeasurementType } from "@/generated/prisma/client"; +import type { + glucoseContextEnum, + measurementTypeEnum, +} from "@/lib/validations/measurement"; +import { classifyExternalId } from "@/lib/validations/external-id"; +import { encryptNote } from "@/lib/crypto/note-cipher"; +import { isP2002, isP2025 } from "@/lib/prisma-errors"; + +import type { JsonImportTally } from "./tally"; + +export interface JsonImportMeasurement { + type: z.infer; + value: number; + unit: string; + measuredAt: Date; + glucoseContext?: z.infer; + source?: string; + notes?: string; + externalId?: string; +} + +export interface ImportedMeasurements { + /** + * Each `(type, measuredAt)` written, so the persistent rollup tier is + * re-folded at the end. The import path once bypassed the rollup write + * hook entirely, so a restore left the `source=rollup` chart paths + * under-counting the imported days until the next worker boot re-ran the + * backfill. + */ + touched: Array<{ type: MeasurementType; measuredAt: Date }>; + /** The rows this import inserted (not the ones it updated in place). */ + inserted: Array<{ id: string; type: MeasurementType; measuredAt: Date }>; +} + +/** + * Write `rows` for `userId`, counting into `tally`. Stops at the first + * write that fails for any reason other than a duplicate, and marks the + * tally failed. + */ +export async function importJsonMeasurements( + userId: string, + rows: JsonImportMeasurement[], + tally: JsonImportTally, +): Promise { + const touched: ImportedMeasurements["touched"] = []; + const inserted: ImportedMeasurements["inserted"] = []; + for (const m of rows) { + // The `(userId, type, source, externalId)` upsert key is only + // idempotent while the id is STABLE; an id that rotates between + // exports re-imports as a fresh row every time. Skipped per entry, + // like every other rejected row in this importer. + const unstable = + m.externalId === undefined ? null : classifyExternalId(m.externalId); + if (unstable) { + tally.unstableShapes.push(unstable); + tally.stats.skipped++; + continue; + } + try { + // `measuredAt` is already a `Date` (validateEntryInstant transform). + const measuredAt = m.measuredAt; + if (m.externalId) { + const notesEncrypted = encryptNote(m.notes || null); + // INSERT ... RETURNING is the only insertion proof. A concurrent + // importer can win this key after any existence probe, so attempt + // the insert first and update only when the statement returns no row. + const created = await prisma.measurement.createManyAndReturn({ + data: [ + { + userId, + type: m.type, + value: m.value, + unit: m.unit, + source: "IMPORT", + externalId: m.externalId, + measuredAt, + notes: null, + notesEncrypted, + glucoseContext: m.glucoseContext ?? null, + }, + ], + skipDuplicates: true, + select: { id: true, type: true, measuredAt: true }, + }); + const row = created[0]; + if (row) { + inserted.push(row); + } else { + // A pre-existing row or a raced insert owns the key. Preserve + // idempotent re-import semantics without classifying this update + // as an arrival. + try { + await prisma.measurement.update({ + where: { + userId_type_source_externalId: { + userId, + type: m.type, + source: "IMPORT", + externalId: m.externalId, + }, + }, + data: { + value: m.value, + unit: m.unit, + measuredAt, + notes: null, + notesEncrypted, + glucoseContext: m.glucoseContext ?? null, + }, + }); + } catch (err) { + if (isP2025(err)) { + tally.stats.skipped++; + continue; + } + throw err; + } + } + } else { + const row = await prisma.measurement.create({ + data: { + userId, + type: m.type, + value: m.value, + unit: m.unit, + source: "IMPORT", + measuredAt, + notes: null, + notesEncrypted: encryptNote(m.notes || null), + glucoseContext: m.glucoseContext ?? null, + }, + select: { id: true, type: true, measuredAt: true }, + }); + inserted.push(row); + } + touched.push({ type: m.type as MeasurementType, measuredAt }); + tally.stats.measurements++; + } catch (err) { + if (isP2002(err)) { + tally.stats.skipped++; + continue; + } + tally.writeFailed = true; + break; + } + } + return { touched, inserted }; +} diff --git a/src/lib/import/json/mood.ts b/src/lib/import/json/mood.ts new file mode 100644 index 000000000..675292a43 --- /dev/null +++ b/src/lib/import/json/mood.ts @@ -0,0 +1,253 @@ +/** + * The mood rows of a JSON import (`POST /api/import`). + * + * A row with an external id upserts on `(userId, source, externalId)`. A row + * without a logged instant gets a stable fallback instant inside its day, + * derived from the row's content and its occurrence in the file, so the same + * file lands on the same instants every time and twin rows land on distinct + * ones. + */ +import { createHash } from "node:crypto"; + +import { prisma } from "@/lib/db"; +import { ENTRY_INSTANT_CLOCK_SKEW_MS } from "@/lib/validations/entry-instant"; +import { classifyExternalId } from "@/lib/validations/external-id"; +import { isP2002 } from "@/lib/prisma-errors"; +import { moodDateKey } from "@/lib/mood/date-key"; +import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; +import { deriveA1 } from "@/lib/mood/level-a"; +import { zonedWallClockToUtc } from "@/lib/tz/wall-clock"; + +import type { JsonImportTally } from "./tally"; + +export interface JsonImportMoodEntry { + date: string; + mood: "SUPER_GUT" | "GUT" | "OKAY" | "SCHLECHT" | "LAUSIG"; + score: number; + tags?: string; + loggedAt?: Date; + externalId?: string; +} + +/** + * The start of a `YYYY-MM-DD` day in the default zone, or null when the + * string does not name a real day. The route's schema uses it to bound a row + * without a logged instant. + */ +export function fallbackMoodDateStart(date: string): Date | null { + const [year, month, day] = date.split("-").map(Number); + if (!year || !month || !day) return null; + const instant = zonedWallClockToUtc( + { year, month, day, hour: 0, minute: 0 }, + DEFAULT_TIMEZONE, + ); + return moodDateKey(instant, DEFAULT_TIMEZONE) === date ? instant : null; +} + +function fallbackMoodIdentity(entry: JsonImportMoodEntry): string { + return JSON.stringify([ + entry.date, + entry.mood, + entry.score, + entry.tags ?? null, + entry.externalId ?? null, + ]); +} + +function stableFallbackMoodInstant( + entry: JsonImportMoodEntry, + occurrence: number, + probe: number, +): Date { + const digest = createHash("sha256") + .update(`${fallbackMoodIdentity(entry)}:${occurrence}`) + .digest(); + const initialOffset = digest.readUInt32BE(0) % ENTRY_INSTANT_CLOCK_SKEW_MS; + const offset = (initialOffset + probe) % ENTRY_INSTANT_CLOCK_SKEW_MS; + const dateStart = fallbackMoodDateStart(entry.date); + if (!dateStart) throw new Error("Invalid mood date"); + return new Date(dateStart.getTime() + offset); +} + +/** The fallback instants this import has handed out, and how often each row content occurred. */ +interface FallbackState { + occurrences: Map; + reserved: Set; +} + +async function writeMoodEntry( + userId: string, + e: JsonImportMoodEntry, + loggedAt: Date, +): Promise { + if (e.externalId) { + // Idempotent re-import keyed on the source-stable id. A second import + // of the same export updates the row in place rather than skipping it as + // a duplicate, so an upstream edit (re-scored mood, added tag) is + // reflected. + await prisma.moodEntry.upsert({ + where: { + userId_source_externalId: { + userId, + source: "IMPORT", + externalId: e.externalId, + }, + }, + update: { + date: e.date, + mood: e.mood, + score: e.score, + // Pleasantness derived from the row's own label, not from the file's + // `score`. This is the one writer whose score comes out of the file + // rather than out of the label, so the two can disagree; the column + // that records a self-assessment has to agree with the label the + // user actually picked. + moodA1: deriveA1(e.mood), + tags: e.tags || null, + moodLoggedAt: loggedAt, + }, + create: { + userId, + date: e.date, + mood: e.mood, + score: e.score, + moodA1: deriveA1(e.mood), + tags: e.tags || null, + source: "IMPORT", + externalId: e.externalId, + moodLoggedAt: loggedAt, + }, + }); + } else { + await prisma.moodEntry.create({ + data: { + userId, + date: e.date, + mood: e.mood, + score: e.score, + // See the upsert arm above: derived from the label, because an + // imported file's `score` is its own field. + moodA1: deriveA1(e.mood), + tags: e.tags || null, + source: "IMPORT", + moodLoggedAt: loggedAt, + }, + }); + } +} + +/** + * Whether the row already stored at `loggedAt` is this very fallback row + * from an earlier import of the same file. Throws when the probe fails. + */ +async function sameImportedRow( + userId: string, + e: JsonImportMoodEntry, + loggedAt: Date, +): Promise { + const existing = await prisma.moodEntry.findUnique({ + where: { + userId_date_moodLoggedAt: { + userId, + date: e.date, + moodLoggedAt: loggedAt, + }, + }, + select: { + source: true, + mood: true, + score: true, + tags: true, + externalId: true, + }, + }); + return ( + existing?.source === "IMPORT" && + existing.mood === e.mood && + existing.score === e.score && + existing.tags === (e.tags || null) && + (existing.externalId ?? null) === null + ); +} + +async function importMoodEntry( + userId: string, + e: JsonImportMoodEntry, + fallback: FallbackState, + tally: JsonImportTally, +): Promise { + // Same stability floor as the measurement rows: the mood upsert keys on + // `(userId, source, externalId)`. + const unstableMood = + e.externalId === undefined ? null : classifyExternalId(e.externalId); + if (unstableMood) { + tally.unstableShapes.push(unstableMood); + tally.stats.skipped++; + return; + } + const identity = e.loggedAt ? null : fallbackMoodIdentity(e); + const occurrence = + identity === null ? 0 : (fallback.occurrences.get(identity) ?? 0); + if (identity !== null) { + fallback.occurrences.set(identity, occurrence + 1); + } + + for (let probe = 0; probe < ENTRY_INSTANT_CLOCK_SKEW_MS; probe++) { + const loggedAt = + e.loggedAt ?? stableFallbackMoodInstant(e, occurrence, probe); + if (!e.loggedAt && fallback.reserved.has(loggedAt.getTime())) { + continue; + } + try { + await writeMoodEntry(userId, e, loggedAt); + if (!e.loggedAt) { + fallback.reserved.add(loggedAt.getTime()); + } + tally.stats.moodEntries++; + return; + } catch (err) { + if (!isP2002(err)) { + tally.writeFailed = true; + return; + } + if (e.loggedAt) { + tally.stats.skipped++; + return; + } + if (!e.externalId) { + try { + if (await sameImportedRow(userId, e, loggedAt)) { + fallback.reserved.add(loggedAt.getTime()); + tally.stats.skipped++; + return; + } + } catch { + tally.writeFailed = true; + return; + } + } + if (probe === ENTRY_INSTANT_CLOCK_SKEW_MS - 1) { + tally.writeFailed = true; + } + } + } +} + +/** + * Write `rows` for `userId`, counting into `tally`. Stops at the first row + * whose write failed for a reason other than a duplicate. + */ +export async function importJsonMoodEntries( + userId: string, + rows: JsonImportMoodEntry[], + tally: JsonImportTally, +): Promise { + const fallback: FallbackState = { + occurrences: new Map(), + reserved: new Set(), + }; + for (const e of rows) { + await importMoodEntry(userId, e, fallback, tally); + if (tally.writeFailed) break; + } +} diff --git a/src/lib/import/json/run.ts b/src/lib/import/json/run.ts new file mode 100644 index 000000000..1f965c4a8 --- /dev/null +++ b/src/lib/import/json/run.ts @@ -0,0 +1,101 @@ +/** + * One JSON import (`POST /api/import`): the measurement rows, then the mood + * rows, then the rollup and status tails, the wide event and the audit row. + * Deduplicates via unique constraints: existing records are skipped or, with + * an external id, updated in place. + */ +import { annotate } from "@/lib/logging/context"; +import { auditLog } from "@/lib/auth/audit"; +import { unstableExternalIdMeta } from "@/lib/validations/external-id"; +import { recomputeUserMoodRollups } from "@/lib/rollups/mood-rollups"; +import { afterMeasurementMutation } from "@/lib/rollups/after-measurement-mutation"; +import { emitInsertedMeasurementArrivals } from "@/lib/arrivals/measurement-emit"; +import { maybeEnqueueMorningRefresh } from "@/lib/daily/morning-refresh-trigger"; + +import { + importJsonMeasurements, + type JsonImportMeasurement, +} from "./measurements"; +import { importJsonMoodEntries, type JsonImportMoodEntry } from "./mood"; +import { newJsonImportTally, type JsonImportTally } from "./tally"; + +export async function runJsonImport(input: { + userId: string; + measurements: JsonImportMeasurement[] | undefined; + moodEntries: JsonImportMoodEntry[] | undefined; + ipAddress: string | null; +}): Promise> { + const { userId } = input; + const tally = newJsonImportTally(); + + const written = input.measurements?.length + ? await importJsonMeasurements(userId, input.measurements, tally) + : { touched: [], inserted: [] }; + + void emitInsertedMeasurementArrivals( + userId, + written.inserted, + "json_import", + ).catch(() => {}); + void maybeEnqueueMorningRefresh( + userId, + written.inserted + .filter((row) => row.type === "SLEEP_DURATION") + .map((row) => row.measuredAt), + ).catch(() => {}); + + if (!tally.writeFailed && input.moodEntries?.length) { + await importJsonMoodEntries(userId, input.moodEntries, tally); + } + + // One bounded re-fold per import is cheaper than firing N per-row hooks. + // The mood rollup tier is keyed on the user + day; the fold materialises + // every touched day from the entries just written. Best-effort: an + // importer hiccup must not surface as a 5xx, the rollup is a cache tier. + if (tally.stats.moodEntries > 0) { + try { + await recomputeUserMoodRollups(userId, { granularities: ["DAY"] }); + } catch (err) { + annotate({ + meta: { + mood_rollup_import_failed: true, + mood_rollup_import_error: + err instanceof Error ? err.message : String(err), + }, + }); + } + } + + // Refresh the persistent measurement rollup table for every distinct + // (type, day) the import touched (collapsed, so a 10 000-row restore pays + // at most ~N recomputes) and re-warm the touched types' status + // assessments. Best-effort: a populator hiccup never fails the importer. + await afterMeasurementMutation(userId, written.touched, { + // A file the person chose to import: a deliberate act, narrated like a + // hand entry rather than held to the sync gap. + source: "manual", + label: "import", + }); + + annotate({ + meta: { + import_stats: tally.stats, + ...(tally.unstableShapes.length > 0 + ? unstableExternalIdMeta("import.json", tally.unstableShapes) + : {}), + }, + }); + + try { + await auditLog("import.upload", { + userId, + ipAddress: input.ipAddress, + details: tally.stats, + }); + } catch (err) { + if (!tally.writeFailed) throw err; + annotate({ meta: { import_audit_failed: true } }); + } + + return { stats: tally.stats, writeFailed: tally.writeFailed }; +} diff --git a/src/lib/import/json/tally.ts b/src/lib/import/json/tally.ts new file mode 100644 index 000000000..243150f06 --- /dev/null +++ b/src/lib/import/json/tally.ts @@ -0,0 +1,17 @@ +import type { UnstableExternalIdShape } from "@/lib/validations/external-id"; + +/** The running counts of one JSON import, shared by its row writers. */ +export interface JsonImportTally { + stats: { measurements: number; moodEntries: number; skipped: number }; + unstableShapes: UnstableExternalIdShape[]; + /** A write failed for a reason other than a duplicate; the import stops. */ + writeFailed: boolean; +} + +export function newJsonImportTally(): JsonImportTally { + return { + stats: { measurements: 0, moodEntries: 0, skipped: 0 }, + unstableShapes: [], + writeFailed: false, + }; +} diff --git a/src/lib/insights/briefing-request/cache.ts b/src/lib/insights/briefing-request/cache.ts new file mode 100644 index 000000000..2673bf402 --- /dev/null +++ b/src/lib/insights/briefing-request/cache.ts @@ -0,0 +1,133 @@ +/** + * The generating POST's cache short-circuit, and the cached payload it + * degrades to when the hour's generation quota is spent. + * + * A fresh cache WITHOUT a briefing must not satisfy a briefing-expecting + * caller: after a failed nightly warm the cached payload can sit inside the + * 24 h window all day, and an unconditional short-circuit served it to every + * POST (three regenerate attempts, three `cached: true` responses, zero + * generations). The short-circuit holds only when the cached payload carries + * today's briefing OR the account has no usable provider (regenerating would + * be futile — serve what exists). When the briefingless fall-through is + * rate-limited, the cached payload is served after all, so a POST-as-read + * client degrades to the old behaviour instead of a 429. + */ +import { isLegacyInsightPayload } from "@/lib/ai/legacy-payload"; +import { + generatedOnLocalToday, + insightsPayloadForToday, +} from "@/lib/daily/briefing-today-read"; +import { readBriefingGeneratedAt } from "@/lib/insights/briefing-generated-at"; +import { cachedPayloadCarriesBriefing } from "@/lib/insights/briefing-payload"; +import { hasUsableStatusProvider } from "@/lib/insights/status-provider"; +import { annotate } from "@/lib/logging/context"; + +import type { GenerationContext } from "./context"; + +/** A fresh cached payload kept aside for the quota fallback. */ +export interface CachedFallback { + cached: unknown; + legacyPayload: boolean; + cachedAt: Date; +} + +export type CacheDecision = + | { kind: "serve"; data: Record } + | { kind: "generate"; fallback: CachedFallback | null }; + +export async function briefingFromCache( + ctx: GenerationContext, + forceRefresh: boolean, +): Promise { + const { dbUser, cachedText, userId, locale } = ctx; + // A cache tagged with another language is not this reader's cache and + // never short-circuits; the generation writes the reader's own. + const cachedLocaleMatches = + dbUser?.insightsCachedLocale == null || + dbUser.insightsCachedLocale === locale; + if ( + !forceRefresh && + dbUser?.insightsCachedAt && + cachedText && + cachedLocaleMatches && + Date.now() - dbUser.insightsCachedAt.getTime() < 24 * 60 * 60 * 1000 + ) { + try { + const cached = JSON.parse(cachedText); + // Legacy payloads (pre-rationale) are flagged so the UI can show a + // "regenerate for new explainability features" CTA. No automatic + // regeneration: that would burn rate-limit tokens on a cache hit + // silently. User-initiated only. + const legacyPayload = isLegacyInsightPayload(cached); + // A briefing generated on an earlier day is not today's read: like a + // briefingless cache, it does not short-circuit a provider-backed + // account, and degrades to the cached payload on a 429. + const carriesBriefing = + cachedPayloadCarriesBriefing(cachedText) && + generatedOnLocalToday( + readBriefingGeneratedAt(cachedText), + ctx.timezone, + new Date(), + ); + if (carriesBriefing || !(await hasUsableStatusProvider(userId))) { + annotate({ + action: { name: "insights.generate" }, + meta: { + cached: true, + legacyPayload, + briefingless: !carriesBriefing, + }, + }); + return { + kind: "serve", + data: { + insights: await insightsPayloadForToday(cached, { + userId, + timezone: ctx.timezone, + language: locale, + }), + cached: true, + cachedAt: dbUser.insightsCachedAt, + legacyPayload, + }, + }; + } + annotate({ + action: { name: "insights.generate.briefingless_cache_bypassed" }, + meta: { locale }, + }); + return { + kind: "generate", + fallback: { + cached, + legacyPayload, + cachedAt: dbUser.insightsCachedAt, + }, + }; + } catch { + // Invalid cache, regenerate + } + } + return { kind: "generate", fallback: null }; +} + +/** The cached payload, served when the hour's quota refused a generation. */ +export async function serveCachedFallback( + ctx: GenerationContext, + fallback: CachedFallback, +): Promise> { + annotate({ + action: { name: "insights.generate" }, + meta: { cached: true, rate_limited_fallback: true }, + }); + return { + insights: await insightsPayloadForToday(fallback.cached, { + userId: ctx.userId, + timezone: ctx.timezone, + language: ctx.locale, + }), + cached: true, + cachedAt: fallback.cachedAt, + legacyPayload: fallback.legacyPayload, + }; +} diff --git a/src/lib/insights/briefing-request/commit.ts b/src/lib/insights/briefing-request/commit.ts new file mode 100644 index 000000000..44141b255 --- /dev/null +++ b/src/lib/insights/briefing-request/commit.ts @@ -0,0 +1,202 @@ +/** + * The on-demand briefing's commit: the outbound safety screen, the two scope + * guards, the cache write, and everything that follows a written briefing. + * + * Contract with the client (`AdvisorSettleState` in + * `src/components/insights/use-insights-advisor.ts`, refs #786): on success + * this writes the fresh briefing to the user cache (`insightsCachedAt` + * advances) — even when the client has long since aborted its 45 s request — + * and a failed inline generation leaves a failure marker the read reports as + * `generationFailed`. The client settles a timed-out regenerate on exactly + * those two signals, so renaming or short-circuiting either write breaks + * that convergence. + */ +import { prisma } from "@/lib/db"; +import { auditLog } from "@/lib/auth/audit"; +import { getSelfContextTextForUser } from "@/lib/ai/coach/about-me"; +import { screenInsightPayloadProse } from "@/lib/ai/safety/insight-payload-screen"; +import { invalidateUserInsights } from "@/lib/cache/invalidate"; +import { insightsPayloadForToday } from "@/lib/daily/briefing-today-read"; +import { recordBriefingFailure } from "@/lib/insights/briefing-failure-marker"; +import { withBriefingGeneratedAt } from "@/lib/insights/briefing-generated-at"; +import { applyBriefingShortForm } from "@/lib/insights/briefing-short-form"; +import { insightsCacheWrite } from "@/lib/insights/cached-text-cipher"; +import { enqueueStatusRefillForUser } from "@/lib/insights/comprehensive-generate"; +import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash"; +import { normalizeLocale } from "@/lib/insights/status-shared"; +import { annotate } from "@/lib/logging/context"; + +import type { BriefingCall } from "./completion"; +import type { BriefingRefusal } from "./context"; +import type { PreparedFeatures } from "./features"; +import type { GroundedReply } from "./grounding"; + +export type CommitOutcome = + | { ok: true; data: Record } + | { ok: false; refusal: BriefingRefusal }; + +/** Write the briefing, guarded on the scope the prompt was built from. */ +async function writeBriefing( + call: BriefingCall, + prepared: PreparedFeatures, + stored: object, +): Promise { + const { ctx, prompt } = call; + const { scope, locale } = ctx; + const committed = await prisma.user.updateMany({ + where: { + id: ctx.userId, + insightsPrivacyMode: scope.mode, + insightsExcludeMetrics: { equals: scope.excludeMetrics }, + locale: scope.locale, + dateOfBirth: scope.dateOfBirth, + gender: scope.gender, + heightCm: scope.heightCm, + displayName: scope.displayName, + }, + data: { + insightsCachedAt: new Date(), + ...insightsCacheWrite(JSON.stringify(stored)), + insightsCachedLocale: locale, + insightsSnapshotHash: hashInsightSnapshot({ + features: prepared.compactFeatures, + aboutMe: prompt.aboutMe ?? null, + comparisonBaseline: prompt.comparisonSnapshot?.baseline ?? "none", + generationLocale: locale, + ...(prompt.briefingForm === "short" + ? { briefingForm: prompt.briefingForm } + : {}), + }), + }, + }); + return committed.count > 0; +} + +/** What follows a written briefing: the marker, the cards, the audit row. */ +async function afterWrite( + call: BriefingCall, + reply: GroundedReply, + ipAddress: string | null, + fallbackHopCount: number, +): Promise { + const { ctx } = call; + const { userId, locale } = ctx; + const { result, workingProviderType } = reply; + // The grounding omission persists (marker AFTER the cache write so it is + // newer than `insightsCachedAt`). The transient `briefingOmittedReason` on + // the response only reached the caller that forced the regenerate; every + // later read showed a generic "no briefing yet". The dated marker lets the + // read surface the same honest "briefing withheld" state until the next + // successful generation. + if (reply.briefingOmittedReason === "ungrounded") { + void recordBriefingFailure({ + userId, + reason: "briefing-ungrounded", + locale, + }); + } + + // No blanket per-status eviction (nuking ~45 cache rows per manual + // regenerate deleted every hash baseline and caused a card-regeneration + // storm). The manual regenerate takes the cards along through the + // hash-gated queue: every scope the user has data for is enqueued, the + // worker forces each generator past its same-day cache read, and the + // content-hash gate regenerates exactly the cards whose data changed while + // refreshing the unchanged ones for free. This is the user's escape hatch + // for a card they can SEE is stale — without it the cards had no refresh + // path until the next nightly warm. + const refillScopes = await enqueueStatusRefillForUser( + userId, + normalizeLocale(locale), + ); + annotate({ + action: { name: "insights.generate.cards_refill" }, + meta: { scopes: refillScopes }, + }); + + // The dashboard snapshot embeds the pre-generated daily briefing + // read-only; drop it so the next snapshot carries the fresh briefing + // instead of the stale one. + invalidateUserInsights(userId); + + await auditLog("insights.generate", { + userId, + ipAddress, + details: { + privacyMode: ctx.dbUser?.insightsPrivacyMode, + tokensUsed: result.tokensUsed, + providerType: result.providerType, + chainProviderType: workingProviderType, + fallbackHopCount, + model: result.model, + }, + }); + + annotate({ + action: { name: "insights.generate" }, + meta: { + cached: false, + providerType: result.providerType, + chainProviderType: workingProviderType, + fallbackHopCount, + model: result.model, + tokensUsed: result.tokensUsed, + }, + }); +} + +export async function commitBriefing( + call: BriefingCall, + prepared: PreparedFeatures, + reply: GroundedReply, + options: { ipAddress: string | null; fallbackHopCount: number }, +): Promise { + const { ctx, prompt } = call; + const { userId, locale } = ctx; + let { insights } = reply; + + // Outbound safety screen over the whole prose surface, before the + // persist. This path is user-initiated but writes the SAME cached payload + // the background generator writes, so a violation must not be persisted + // here either. The user is waiting, so unlike the background path they get + // an explicit error rather than silence — and because nothing was written, + // the previous cached insight is still what the app renders. + const screened = screenInsightPayloadProse(insights, locale); + if (screened) { + annotate({ + action: { name: "insights.generate.outbound_blocked" }, + meta: { locale, reason: screened }, + }); + return { ok: false, refusal: "outbound-screened" }; + } + + const currentSelfContext = await getSelfContextTextForUser(userId, locale); + if (currentSelfContext !== prompt.aboutMe) { + return { ok: false, refusal: "profile-scope-changed" }; + } + if (prompt.briefingForm === "short") { + insights = applyBriefingShortForm(insights); + } + const stored = withBriefingGeneratedAt(insights, new Date()); + if (!(await writeBriefing(call, prepared, stored))) { + return { ok: false, refusal: "scope-changed" }; + } + + await afterWrite(call, reply, options.ipAddress, options.fallbackHopCount); + + return { + ok: true, + data: { + insights: await insightsPayloadForToday(stored, { + userId, + timezone: ctx.timezone, + language: locale, + }), + cached: false, + legacyPayload: false, + // Additive: non-null when the grounding gate stripped the briefing + // from THIS generation, so the card can explain the hole (#470). + briefingOmittedReason: reply.briefingOmittedReason, + }, + }; +} diff --git a/src/lib/insights/briefing-request/completion.ts b/src/lib/insights/briefing-request/completion.ts new file mode 100644 index 000000000..a63f86804 --- /dev/null +++ b/src/lib/insights/briefing-request/completion.ts @@ -0,0 +1,192 @@ +/** + * The on-demand briefing's provider calls, and what a failed call means for + * the person waiting on it. + */ +import { resolveInsightsMaxTokens } from "@/lib/ai/ai-budgets"; +import { providerFailureKind } from "@/lib/ai/failure-kind"; +import { buildSystemPromptWithReferences } from "@/lib/ai/prompts/insight-generator"; +import { + AllProvidersFailedError, + type ProviderChainResolved, + type RunRawWithFallbackResult, +} from "@/lib/ai/provider-runner"; +import { + BriefingBudgetExceededError, + runBriefingCompletion, +} from "@/lib/insights/briefing-provider"; +import { recordBriefingFailure } from "@/lib/insights/briefing-failure-marker"; +import { annotate } from "@/lib/logging/context"; + +import type { BriefingRefusal, GenerationContext } from "./context"; +import type { BriefingPrompt } from "./prompt"; + +export interface BriefingCall { + ctx: GenerationContext; + chain: ProviderChainResolved[]; + prompt: BriefingPrompt; +} + +/** One briefing completion with the route's fixed parameters. */ +export function runBriefingCall( + call: BriefingCall, + options: { + userPrompt: string; + stage: "generate" | "grounding-retry"; + reasoning: boolean; + }, +): Promise { + const { ctx, chain, prompt } = call; + return runBriefingCompletion({ + userId: ctx.userId, + chain, + // The strict prompt carries GROUND RULE 8 — emit a top-level + // `dailyBriefing` block when the snapshot has analysable signal — plus + // the trendAnnotations and storyboardAnnotations rules. The legacy + // `getInsightsSystemPrompt` returned the old `{changed, stable, drivers, + // …}` shape and never asked the model for dailyBriefing, so the hero + // strip's "Re-run analysis" button kept producing a payload with no + // briefing block. The reply tolerates the strict shape via + // `insightResultSchema.safeParse`'s soft fallback to the parsed object. + systemPrompt: buildSystemPromptWithReferences( + ctx.locale, + prompt.referenceMetrics, + ), + userPrompt: options.userPrompt, + temperature: 0.3, + // Env-tunable output ceiling (INSIGHTS_MAX_TOKENS, default 2500, #470). + // A fixed 1500 truncated verbose models' briefing JSON mid-string → + // generic invalid-JSON 422. + maxTokens: resolveInsightsMaxTokens(), + // A wide upstream budget, honouring the per-user response-timeout + // setting, so the reasoning-heavy briefing generation is not aborted at + // the client's 60 s default on large accounts. + timeoutMs: ctx.effectiveTimeoutMs, + stage: options.stage, + // A briefing asked for by hand reasons exactly like the scheduled one; + // the retries repair a reply and do not. + ...(options.reasoning ? { reasoningJob: "daily_briefing" as const } : {}), + }); +} + +export type FirstCompletion = + | { + ok: true; + result: RunRawWithFallbackResult["result"]; + workingProviderType: string; + fallbackHopCount: number; + } + | { ok: false; refusal: BriefingRefusal }; + +/** The refusal for a chain on which every provider failed. */ +function chainRefusal(e: AllProvidersFailedError): BriefingRefusal { + annotate({ + meta: { + insights_chain_outcome: "all-failed", + insights_chain_attempts: e.attempts.length, + insights_chain_first_provider: e.attempts[0]?.providerType ?? null, + insights_chain_first_status: e.attempts[0]?.httpStatus ?? null, + }, + }); + // The most specific user-facing message available. Auth-class failures + // across the entire chain mean every provider's credential is bad — the + // "check your API key" wording tells the user where to act. 429 across the + // chain is rate-limit; 5xx is upstream brown-out; transport-only + // (ECONNRESET, no httpStatus) maps to the generic connection hint so users + // on flaky networks don't see "rate-limited". + if (e.attempts.every((a) => a.httpStatus === 401 || a.httpStatus === 403)) { + return "provider-rejected"; + } + if (e.attempts.every((a) => a.httpStatus === 429)) { + return "provider-rate-limited"; + } + if (e.attempts.some((a) => a.httpStatus !== null && a.httpStatus >= 500)) { + return "provider-unavailable"; + } + if (e.attempts.every((a) => a.httpStatus === null || a.httpStatus <= 0)) { + return "provider-connection"; + } + return "provider-unavailable"; +} + +/** + * The refusal for a single provider error that did not cascade (e.g. a + * custom 4xx from a self-hosted local model). Cloudflare rewrites 5xx to its + * own HTML error page, which breaks `await res.json()` on the dashboard, so + * the statuses map onto the ai/test categorisation. + */ +function providerRefusal(e: unknown): BriefingRefusal { + const err = e as Error & { httpStatus?: number; bodyExcerpt?: string }; + annotate({ + meta: { + insights_provider_error: err.message.slice(0, 500), + insights_provider_status: err.httpStatus ?? null, + insights_provider_body_excerpt: err.bodyExcerpt?.slice(0, 500) ?? null, + }, + }); + const status = err.httpStatus ?? 0; + if (status === 401 || status === 403) return "provider-rejected"; + if (status === 429) return "provider-rate-limited"; + if (status >= 500) return "provider-unavailable"; + return "provider-connection"; +} + +export async function runFirstCompletion( + call: BriefingCall, +): Promise { + try { + const fallback = await runBriefingCall(call, { + userPrompt: call.prompt.userPrompt, + stage: "generate", + reasoning: true, + }); + return { + ok: true, + result: fallback.result, + workingProviderType: fallback.workingProvider.providerType, + fallbackHopCount: fallback.fallbackHops.length, + }; + } catch (e) { + // The day's token ceiling refused the reservation — no provider was + // contacted, so this is NOT a provider failure and must not record a + // briefing-failure marker (which would make the read path claim the + // refresh broke). The hourly rate limit bounds request frequency; this + // bounds cost. + if (e instanceof BriefingBudgetExceededError) { + return { ok: false, refusal: "budget-exceeded" }; + } + // A dated failure marker (no cache row is written on this path, so the + // last good briefing stays intact). The read path pairs the preserved + // text with a discreet "couldn't refresh" hint, or shows a retry empty + // state when there is no last good text. The upstream status rides + // along so the hint can tell an auth / rate-limit failure apart from a + // plain timeout: for an all-providers-failed error the first hop's + // status is the most representative; a single provider error exposes its + // own `httpStatus`. + const failureHttpStatus = + e instanceof AllProvidersFailedError + ? (e.attempts[0]?.httpStatus ?? null) + : typeof (e as { httpStatus?: number }).httpStatus === "number" + ? (e as { httpStatus?: number }).httpStatus + : null; + void recordBriefingFailure({ + userId: call.ctx.userId, + reason: + e instanceof AllProvidersFailedError + ? "all-providers-failed" + : "provider-error", + locale: call.ctx.locale, + httpStatus: failureHttpStatus, + failureKind: + e instanceof AllProvidersFailedError + ? (e.attempts[0]?.failureKind ?? null) + : providerFailureKind(e).failure, + }); + return { + ok: false, + refusal: + e instanceof AllProvidersFailedError + ? chainRefusal(e) + : providerRefusal(e), + }; + } +} diff --git a/src/lib/insights/briefing-request/context.ts b/src/lib/insights/briefing-request/context.ts new file mode 100644 index 000000000..7c488fa3c --- /dev/null +++ b/src/lib/insights/briefing-request/context.ts @@ -0,0 +1,137 @@ +/** + * What an on-demand briefing generation reads before it decides anything: + * the account row, the cached payload, the scope columns the commit guard + * matches on, the response timeout, the reader's language and zone. + */ +import type { NextRequest } from "next/server"; + +import type { Prisma } from "@/generated/prisma/client"; +import { prisma } from "@/lib/db"; +import { AI_BUDGETS } from "@/lib/ai/ai-budgets"; +import { resolveEffectiveTimeoutMs } from "@/lib/ai/effective-timeout"; +import type { Locale } from "@/lib/i18n/config"; +import { resolveServerLocale } from "@/lib/i18n/server-locale"; +import { + INSIGHTS_CACHE_SELECT, + openInsightsCachedText, +} from "@/lib/insights/cached-text-cipher"; +import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; + +export const GENERATION_USER_SELECT = { + insightsPrivacyMode: true, + insightsCachedAt: true, + ...INSIGHTS_CACHE_SELECT, + insightsCachedLocale: true, + timezone: true, + // The per-user opt-out list mirroring the Coach settings. Filtered off + // `features` before serialisation so the model never sees the excluded + // blocks. + insightsExcludeMetrics: true, + locale: true, + // The profile for the derived-signal briefing detector. + dateOfBirth: true, + gender: true, + heightCm: true, + // The first name for the sparse, hash-gated briefing opener. + displayName: true, + // The per-user response timeout (seconds), threaded onto the generation + // calls so a slow self-hosted backend honours the setting. + aiResponseTimeoutSeconds: true, + // The reader's units; the feature set is converted into them before it is + // serialised, hashed and checked for grounding. + unitPreference: true, + glucoseUnit: true, +} as const; + +export type GenerationUserRow = Prisma.UserGetPayload<{ + select: typeof GENERATION_USER_SELECT; +}>; + +/** + * The columns the commit guard matches on, captured at read time. + * + * Every User write bumps `updatedAt`, including `resolveProviderChain()`'s + * OAuth token-refresh update later in the same request, so keying the + * optimistic-concurrency check on it produced a false 409 whenever a + * provider credential happened to refresh mid-generation. These are exactly + * the selected columns that feed the prompt; requiring an exact match at + * commit still catches a genuine privacy-mode / exclude-list / locale / + * profile change without colliding on unrelated column writes. + */ +export interface GenerationScope { + mode: GenerationUserRow["insightsPrivacyMode"]; + excludeMetrics: GenerationUserRow["insightsExcludeMetrics"]; + locale: GenerationUserRow["locale"]; + dateOfBirth: GenerationUserRow["dateOfBirth"]; + gender: GenerationUserRow["gender"]; + heightCm: GenerationUserRow["heightCm"]; + displayName: GenerationUserRow["displayName"]; +} + +export interface GenerationContext { + userId: string; + dbUser: GenerationUserRow | null; + cachedText: string | null; + scope: GenerationScope; + effectiveTimeoutMs: number; + locale: Locale; + timezone: string; +} + +/** + * Why a generation ended without a briefing. The route owns the status and + * the wording for each. + */ +export type BriefingRefusal = + | "payload-too-large" + | "budget-exceeded" + | "provider-rejected" + | "provider-rate-limited" + | "provider-unavailable" + | "provider-connection" + | "response-truncated" + | "response-invalid-json" + | "outbound-screened" + | "profile-scope-changed" + | "scope-changed"; + +export async function loadGenerationContext(args: { + request: NextRequest; + userId: string; + userLocale: string | null; +}): Promise { + const { request, userId } = args; + const dbUser = await prisma.user.findUnique({ + where: { id: userId }, + select: GENERATION_USER_SELECT, + }); + const cachedText = openInsightsCachedText(dbUser); + const scope: GenerationScope = { + mode: dbUser?.insightsPrivacyMode ?? "aggregated", + excludeMetrics: dbUser?.insightsExcludeMetrics ?? [], + locale: dbUser?.locale ?? null, + dateOfBirth: dbUser?.dateOfBirth ?? null, + gender: dbUser?.gender ?? null, + heightCm: dbUser?.heightCm ?? null, + displayName: dbUser?.displayName ?? null, + }; + + const effectiveTimeoutMs = resolveEffectiveTimeoutMs( + dbUser?.aiResponseTimeoutSeconds, + AI_BUDGETS.comprehensive.timeoutMs, + ); + + const locale = await resolveServerLocale({ + request, + userLocale: dbUser?.locale ?? args.userLocale ?? null, + }); + return { + userId, + dbUser, + cachedText, + scope, + effectiveTimeoutMs, + locale, + timezone: dbUser?.timezone ?? DEFAULT_TIMEZONE, + }; +} diff --git a/src/lib/insights/briefing-request/features.ts b/src/lib/insights/briefing-request/features.ts new file mode 100644 index 000000000..d8e5be35f --- /dev/null +++ b/src/lib/insights/briefing-request/features.ts @@ -0,0 +1,150 @@ +/** + * The feature set an on-demand briefing is written from: the bounded read, + * its downgrade ladder for an oversized payload, the user's exclusions, the + * reader's units, and the compact wire form. + */ +import { compactSections } from "@/lib/ai/prompts/compact-sections"; +import { applyInsightsExcludeFilter } from "@/lib/insights/exclude-filter"; +import { getCachedFeatures } from "@/lib/insights/feature-cache"; +import { + BRIEFING_FEATURE_WINDOW_DAYS, + extractFeatures, + FeaturesPayloadTooLargeError, +} from "@/lib/insights/features"; +import { featuresInReaderUnits } from "@/lib/insights/features-units"; +import { annotate } from "@/lib/logging/context"; +import { resolveUnitPreferences } from "@/lib/measurements/display-transform"; + +import type { GenerationContext } from "./context"; + +export type BriefingFeatures = Awaited>; + +/** + * The blocks dropped when even the aggregated shape crosses the ceiling + * (very large medication history + multi-year context), before one last try. + */ +const MAX_DOWNGRADE_TOKENS: ReadonlyArray = [ + "anthropometrics", + "medications", + "compliance", + "sleep", + "steps", + "hrv", + "resting_hr", +]; + +interface Downgrade { + payloadDowngraded: boolean; + payloadHardDowngraded: boolean; + payloadOversizeBytes: number | null; +} + +/** + * The bounded feature read, computed once per request through the scope the + * route opened. `extractFeatures` enforces a 5 MB ceiling on the serialised + * payload and throws `FeaturesPayloadTooLargeError` past it; the raw + * (bucketed) shape downgrades to the aggregated one rather than 500-ing, so + * the user still gets an insight, and the annotation surfaces the + * regression to ops. Null when every rung is still too large. + */ +async function readWithDowngrade( + userId: string, + includeRaw: boolean, + downgrade: Downgrade, +): Promise { + // The bulk read is bounded to a recent window (all-time extremes are + // sourced separately, see features.ts); the ladder is the safety net for + // the rare oversize payload. + const featureWindow = { sinceDays: BRIEFING_FEATURE_WINDOW_DAYS }; + const read = (raw: boolean) => + getCachedFeatures({ + userId, + includeRaw: raw, + sinceDays: featureWindow.sinceDays, + compute: () => extractFeatures(userId, raw, featureWindow), + }); + try { + return await read(includeRaw); + } catch (err) { + if (!(err instanceof FeaturesPayloadTooLargeError)) throw err; + downgrade.payloadDowngraded = true; + downgrade.payloadOversizeBytes = err.sizeBytes; + } + try { + return await read(false); + } catch (retryErr) { + if (!(retryErr instanceof FeaturesPayloadTooLargeError)) throw retryErr; + // The aggregated shape ALSO crossed the ceiling. Drop the optional + // context blocks via the exclude filter and try once more. + downgrade.payloadHardDowngraded = true; + downgrade.payloadOversizeBytes = retryErr.sizeBytes; + } + try { + return applyInsightsExcludeFilter(await read(false), MAX_DOWNGRADE_TOKENS); + } catch (finalErr) { + // Even the last read blew up. Annotate and refuse rather than let the + // uncaught throw 500 out. + annotate({ + meta: { + insights_payload_too_large: true, + insights_payload_oversize_bytes: + finalErr instanceof FeaturesPayloadTooLargeError + ? finalErr.sizeBytes + : downgrade.payloadOversizeBytes, + }, + }); + return null; + } +} + +export interface PreparedFeatures { + features: BriefingFeatures; + compactFeatures: Record; + featuresJson: string; + units: ReturnType; +} + +export async function prepareBriefingFeatures( + ctx: GenerationContext, +): Promise { + const { dbUser, userId } = ctx; + const includeRaw = dbUser?.insightsPrivacyMode === "raw"; + const downgrade: Downgrade = { + payloadDowngraded: false, + payloadHardDowngraded: false, + payloadOversizeBytes: null, + }; + let features = await readWithDowngrade(userId, includeRaw, downgrade); + if (features === null) return null; + // The user's exclude-metrics opt-out list, applied BEFORE serialisation so + // the model never sees the dropped blocks. + const excludeList = dbUser?.insightsExcludeMetrics ?? []; + features = applyInsightsExcludeFilter(features, excludeList); + // The reader's units, after all canonical arithmetic: the prompt, the + // stored hash and the grounding check read the same converted figures. + const units = resolveUnitPreferences({ + unitPreference: dbUser?.unitPreference, + glucoseUnit: dbUser?.glucoseUnit, + }); + features = featuresInReaderUnits(features, units); + // Zero-row blocks are dropped so the prompt never carries labelled-empty + // sections (`"sleep": []`, `"medications": []`, etc.), which made the + // model narrate "there are no medications in your data" when the user + // explicitly excluded the block. + const compactFeatures = compactSections( + features as unknown as Record, + ); + // Compact on the wire: indentation is about 40% of a pretty-printed + // payload, and the model reads the structure without it (#1217). + const featuresJson = JSON.stringify(compactFeatures); + if (downgrade.payloadDowngraded) { + annotate({ + meta: { + insights_features_downgraded: true, + insights_features_oversize_bytes: downgrade.payloadOversizeBytes, + insights_features_hard_downgraded: downgrade.payloadHardDowngraded, + }, + }); + } + return { features, compactFeatures, featuresJson, units }; +} diff --git a/src/lib/insights/briefing-request/generate.ts b/src/lib/insights/briefing-request/generate.ts new file mode 100644 index 000000000..4ab6029be --- /dev/null +++ b/src/lib/insights/briefing-request/generate.ts @@ -0,0 +1,96 @@ +/** + * An on-demand briefing generation, from the resolved provider chain to the + * committed payload. The route owns authentication, the capability gate, + * the body, the hourly quota and the status of every refusal. + */ +import type { ProviderChainResolved } from "@/lib/ai/provider-runner"; +import { assertConsentForChain } from "@/lib/ai/consent-guard"; +import { resolveProvider, resolveProviderChain } from "@/lib/ai/provider"; +import { recordBriefingFailure } from "@/lib/insights/briefing-failure-marker"; +import { annotate } from "@/lib/logging/context"; + +import { commitBriefing } from "./commit"; +import { runFirstCompletion, type BriefingCall } from "./completion"; +import type { BriefingRefusal, GenerationContext } from "./context"; +import { prepareBriefingFeatures } from "./features"; +import { groundBriefing, parseBriefingReply } from "./grounding"; +import { buildBriefingPrompt } from "./prompt"; + +/** + * The user's full provider chain, so a hard failure on the primary (401, + * 5xx, network) cascades to the next configured fallback rather than + * surfacing to the dashboard. An empty chain falls back to the + * single-provider resolution when the legacy `aiProvider` field still points + * at something usable, so accounts that never customised the chain keep + * working. Null when there is no provider at all. + * + * The consent gate runs on the resolved chain before anything leaves for a + * model. Required for the operator's global key (`admin-openai`); BYOK / + * local / ChatGPT-OAuth chains stay ungated. It throws ConsentRequiredError, + * which `apiHandler` answers with 403 + `consent.ai.required`. + */ +export async function resolveConsentedBriefingChain( + userId: string, +): Promise { + const chain = await resolveProviderChain(userId); + if (chain.length === 0) { + const legacy = await resolveProvider(userId); + if (legacy.type === "none") return null; + chain.push({ providerType: "admin-openai", instance: legacy }); + } + await assertConsentForChain({ userId, chain, surface: "insights" }); + return chain; +} + +export type GenerateOutcome = + | { ok: true; data: Record } + | { ok: false; refusal: BriefingRefusal }; + +export async function generateBriefing(args: { + ctx: GenerationContext; + chain: ProviderChainResolved[]; + ipAddress: string | null; +}): Promise { + const { ctx, chain } = args; + const prepared = await prepareBriefingFeatures(ctx); + if (prepared === null) return { ok: false, refusal: "payload-too-large" }; + + const prompt = await buildBriefingPrompt(ctx, prepared); + const call: BriefingCall = { ctx, chain, prompt }; + + const first = await runFirstCompletion(call); + if (!first.ok) return first; + + let insights; + try { + insights = parseBriefingReply(first.result.content); + } catch { + // A 502 here triggers Cloudflare's HTML error rewrite, which breaks + // `await res.json()` on the client; the refusal stays a JSON 422 so the + // React Query mutation can read the body and surface a readable message. + void recordBriefingFailure({ + userId: ctx.userId, + reason: "invalid-json", + locale: ctx.locale, + }); + // Truncation-aware (#470): `finishReason === "length"` means the model + // hit the output-token ceiling and the JSON was cut off mid-string, which + // is a budget problem, not a model-quality one. + if (first.result.finishReason === "length") { + annotate({ meta: { insights_response_truncated: true } }); + return { ok: false, refusal: "response-truncated" }; + } + return { ok: false, refusal: "response-invalid-json" }; + } + + const reply = await groundBriefing(call, prepared, { + insights, + result: first.result, + workingProviderType: first.workingProviderType, + }); + + return commitBriefing(call, prepared, reply, { + ipAddress: args.ipAddress, + fallbackHopCount: first.fallbackHopCount, + }); +} diff --git a/src/lib/insights/briefing-request/grounding.ts b/src/lib/insights/briefing-request/grounding.ts new file mode 100644 index 000000000..d78653519 --- /dev/null +++ b/src/lib/insights/briefing-request/grounding.ts @@ -0,0 +1,127 @@ +/** + * The reply's parse and the daily-briefing number-grounding gate. + * + * The schema only checks the briefing's SHAPE; the gate asserts every + * number the briefing prose restates traces to a `features.signalsOfDay` + * figure, mirroring the recommendations citation cross-check. On a miss: one + * corrective retry, then the briefing block is dropped rather than shipping + * a fabricated number. + */ +import { + buildBriefingGroundingCorrection, + findUngroundedBriefingNumbers, + readBriefingBlock, +} from "@/lib/ai/briefing-grounding"; +import type { RunRawWithFallbackResult } from "@/lib/ai/provider-runner"; +import { insightResultSchema, type InsightResult } from "@/lib/ai/types"; +import { annotate } from "@/lib/logging/context"; + +import { runBriefingCall, type BriefingCall } from "./completion"; +import type { PreparedFeatures } from "./features"; + +export type BriefingInsights = InsightResult | Record; + +/** The parsed reply: the strict shape when it validates, the raw object if not. */ +export function parseBriefingReply(content: string): BriefingInsights { + const parsed = JSON.parse(content); + // Try the strict schema first, fall back to the raw parsed object. + const validated = insightResultSchema.safeParse(parsed); + return validated.success ? validated.data : parsed; +} + +export interface GroundedReply { + insights: BriefingInsights; + result: RunRawWithFallbackResult["result"]; + workingProviderType: string; + /** + * Non-null when the gate stripped the briefing from THIS generation and no + * previous one could stand in, so the card can explain the hole instead + * of reading "no briefing yet" (#470). + */ + briefingOmittedReason: "ungrounded" | null; +} + +/** The previous cached briefing, when the payload parses and carries one. */ +function previousBriefing(cachedText: string | null): unknown { + if (!cachedText) return null; + try { + const prev = JSON.parse(cachedText) as Record; + return prev.dailyBriefing ?? null; + } catch { + // Unparseable previous payload — nothing safe to stand in. + return null; + } +} + +export async function groundBriefing( + call: BriefingCall, + prepared: PreparedFeatures, + reply: Omit, +): Promise { + const { features } = prepared; + const { comparisonSnapshot } = call.prompt; + let { insights, result, workingProviderType } = reply; + let briefingOmittedReason: "ungrounded" | null = null; + const signals = features.signalsOfDay ?? null; + let ungrounded = findUngroundedBriefingNumbers( + readBriefingBlock(insights), + signals, + features, + comparisonSnapshot, + ); + if (ungrounded.length > 0) { + annotate({ + action: { name: "insights.generate.briefing_grounding_retry" }, + meta: { ungroundedCount: ungrounded.length }, + }); + try { + const retry = await runBriefingCall(call, { + userPrompt: `${call.prompt.userPrompt}\n\n${buildBriefingGroundingCorrection(ungrounded)}`, + stage: "grounding-retry", + reasoning: false, + }); + const retryInsights = parseBriefingReply(retry.result.content); + ungrounded = findUngroundedBriefingNumbers( + readBriefingBlock(retryInsights), + signals, + features, + comparisonSnapshot, + ); + if (ungrounded.length === 0) { + insights = retryInsights; + result = retry.result; + workingProviderType = retry.workingProvider.providerType; + } + } catch { + // Retry failed to produce parseable JSON, or the day's ceiling refused + // the correction pass — either way fall through to the strip. An + // ungrounded figure is never persisted. + } + } + // Still ungrounded after the retry: the freshly generated briefing is + // discarded rather than shipped — the anti-fabrication guarantee is + // absolute here too. But rather than leave a hole, the PREVIOUS cached + // briefing stands in its place: it passed this identical gate when it was + // written, so it carries no fabricated figure, and a day-old grounded + // briefing beats a vanished one. This mirrors the background generator, + // which is the path that writes the same cached payload; the two must not + // disagree on disposal. + // + // `briefingOmittedReason` is set ONLY when no previous briefing exists to + // stand in, so the card's honest "a figure couldn't be verified" state + // still fires exactly when the user really has no briefing. + if (ungrounded.length > 0 && insights && typeof insights === "object") { + const fallbackBriefing = previousBriefing(call.ctx.cachedText); + (insights as Record).dailyBriefing = fallbackBriefing; + briefingOmittedReason = fallbackBriefing == null ? "ungrounded" : null; + annotate({ + action: { name: "insights.generate.briefing_grounding_stripped" }, + meta: { + ungroundedCount: ungrounded.length, + briefing_fallback: + fallbackBriefing != null ? "previous-cached" : "stripped", + }, + }); + } + return { insights, result, workingProviderType, briefingOmittedReason }; +} diff --git a/src/lib/insights/briefing-request/prompt.ts b/src/lib/insights/briefing-request/prompt.ts new file mode 100644 index 000000000..cfebd7e88 --- /dev/null +++ b/src/lib/insights/briefing-request/prompt.ts @@ -0,0 +1,129 @@ +/** + * The on-demand briefing's prompt: the feature snapshot, and every + * SYSTEM CONTEXT block that rides on it. + */ +import { metricsFromPresentSections } from "@/lib/ai/medical-references"; +import { + buildAboutMeInsightBlock, + getSelfContextTextForUser, +} from "@/lib/ai/coach/about-me"; +import { buildBriefingPersonalisationBlock } from "@/lib/ai/prompts/insight-generator"; +import { buildUserPrompt } from "@/lib/ai/prompts/insight-system-prompt"; +import { getAgeFromDateOfBirth } from "@/lib/analytics/pulse-targets"; +import { + briefingFormForRecord, + buildBriefingShortFormBlock, +} from "@/lib/insights/briefing-short-form"; +import { buildComparisonSnapshotForUser } from "@/lib/insights/comprehensive-generate"; +import { + buildDerivedBriefingPrompt, + detectDerivedBriefingSignals, +} from "@/lib/insights/derived-briefing"; +import { + buildGlp1PlateauPrompt, + detectGlp1Plateau, +} from "@/lib/insights/glp1-plateau"; +import { + buildBriefingIllnessCycleContext, + buildBriefingIllnessCyclePrompt, +} from "@/lib/insights/illness-cycle-briefing"; +import { toProfileSex } from "@/lib/profile/sex"; +import { resolveUserTimezone } from "@/lib/tz/resolver"; + +import type { GenerationContext } from "./context"; +import type { PreparedFeatures } from "./features"; + +export type BriefingPrompt = Awaited>; + +export async function buildBriefingPrompt( + ctx: GenerationContext, + prepared: PreparedFeatures, +) { + const { userId, dbUser, locale } = ctx; + const { features, featuresJson, units } = prepared; + // The persisted comparison toggle: when active, a compact prior-period + // snapshot built from the `features` summaries that already include + // `avg30LastMonth` / `avg30LastYear`. The narrative is default-on whenever + // the toggle is on — the pulldown is the single affordance. + const comparisonSnapshot = await buildComparisonSnapshotForUser(userId); + // GLP-1 plateau detector: a SYSTEM CONTEXT block when the active GLP-1 + // medication has been on a stable dose for ≥21 days with the weight delta + // within ±0.5 kg, asking for an observational glp1_plateau keyFinding (no + // dose recommendation per GROUND RULE 13). Null — the common case — costs + // no tokens. + const plateauContext = await detectGlp1Plateau(userId); + let userPrompt = buildUserPrompt( + featuresJson, + dbUser?.insightsPrivacyMode ?? "aggregated", + locale, + comparisonSnapshot ?? undefined, + ); + if (plateauContext) { + userPrompt += buildGlp1PlateauPrompt(plateauContext, locale, units.system); + } + // The user-authored "about me" self-description (Settings → AI), as a + // delimited, user-provided SYSTEM CONTEXT block. Null (no text / + // undecryptable) costs nothing. + const aboutMe = await getSelfContextTextForUser(userId, locale); + if (aboutMe) { + userPrompt += buildAboutMeInsightBlock(aboutMe, locale); + } + // A notable derived wellness signal (readiness / recovery shift, + // confidence-gated), read from the one derived contract; null (the common + // case) costs nothing. + const derivedSex = toProfileSex(dbUser?.gender); + const derivedBriefing = await detectDerivedBriefingSignals(userId, { + ageYears: getAgeFromDateOfBirth(dbUser?.dateOfBirth ?? null), + sex: derivedSex, + heightCm: dbUser?.heightCm ?? null, + }); + if (derivedBriefing) { + userPrompt += buildDerivedBriefingPrompt(derivedBriefing, locale); + } + + // Illness-episode + cycle state, so a mid-illness or mid-cycle user gets + // context-aware prose. The same server-authoritative builders the Coach + // assembles; both are module-gated and short-circuit to null (zero token + // cost) for users without them. + const illnessCycleCtx = await buildBriefingIllnessCycleContext( + userId, + dbUser?.gender ?? null, + await resolveUserTimezone(userId), + ); + if (illnessCycleCtx) { + userPrompt += buildBriefingIllnessCyclePrompt(illnessCycleCtx, locale); + } + + // Opener-archetype rotation + sparse first-name personalization + // (deterministic per user+day; omitted for unnamed accounts). + userPrompt += buildBriefingPersonalisationBlock( + userId, + dbUser?.displayName ?? null, + locale, + await resolveUserTimezone(userId), + ); + // The short form on the `frugal` usage level; the full contract on every + // other level (#1217). + const briefingForm = await briefingFormForRecord(userId); + userPrompt += buildBriefingShortFormBlock(locale, briefingForm); + + // The curated SOURCES block for the metric sections this briefing + // actually carries, so a normative claim can cite a real `referenceId` the + // schema + UI footnote already support. The plain prompt stays unchanged + // when no applicable metric section is present. + const referenceMetrics = metricsFromPresentSections({ + bloodPressure: features.bloodPressure != null, + weight: features.weight != null, + pulse: features.pulse != null, + mood: features.mood != null, + medication: (features.medications?.length ?? 0) > 0, + }); + + return { + userPrompt, + comparisonSnapshot, + aboutMe, + briefingForm, + referenceMetrics, + }; +} diff --git a/src/lib/insights/briefing-request/read.ts b/src/lib/insights/briefing-request/read.ts new file mode 100644 index 000000000..23a8b7e91 --- /dev/null +++ b/src/lib/insights/briefing-request/read.ts @@ -0,0 +1,224 @@ +/** + * The read-only advisor briefing: the cached payload, served immediately and + * never generated here. + * + * The advisor consumers (hero strip, daily-briefing card, trends row) used + * to mount against the generating POST, which blocked the page-load path on + * the full provider chain. This read serves `User.insightsCachedTextEncrypted` + * as it is and, when the cache is stale or missing AND a provider is + * configured, enqueues an out-of-band warm (the same `insight-pregenerate` + * queue the nightly cron and the "prepare assessments" button use). The next + * read reflects the fresh briefing. + */ +import type { NextRequest } from "next/server"; + +import { prisma } from "@/lib/db"; +import { + generatedOnLocalToday, + insightsPayloadForToday, +} from "@/lib/daily/briefing-today-read"; +import { isLegacyInsightPayload } from "@/lib/ai/legacy-payload"; +import { resolveServerLocale } from "@/lib/i18n/server-locale"; +import { readBriefingFailure } from "@/lib/insights/briefing-failure-marker"; +import { readBriefingGeneratedAt } from "@/lib/insights/briefing-generated-at"; +import { + INSIGHTS_CACHE_SELECT, + openInsightsCachedText, +} from "@/lib/insights/cached-text-cipher"; +import { normalizeLocale } from "@/lib/insights/status-shared"; +import { hasUsableStatusProvider } from "@/lib/insights/status-provider"; +import { enqueueForceWarm } from "@/lib/jobs/insight-pregenerate-shared"; +import { annotate } from "@/lib/logging/context"; +import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; + +/** + * Briefing freshness window for the read. A cached briefing read this + * recently is fresh and triggers no warm; older / missing enqueues an + * out-of-band regeneration. Mirrors the 24 h cache-hit window the POST path + * honours so the two stay consistent. + */ +const BRIEFING_FRESH_MS = 24 * 60 * 60 * 1000; + +/** + * The honesty signals beside the payload: whether a provider exists, and + * what the last generation attempt left behind. + */ +async function readGenerationSignals(userId: string, cachedAt: Date | null) { + // Provider availability, resolved once. The read never blocks on or + // generates through the provider, but it must REPORT whether one exists: + // a provider-less account's cached briefing freezes at the last successful + // generation (the pregenerate cron can't refill it), and the UI presented + // that stale text as current. `hasProvider: false` lets the insights + // surfaces pair the honest "Stand: vor X Tagen" age with a discreet + // connect-an-AI-provider hint instead of implying the read is live. + const hasProvider = await hasUsableStatusProvider(userId); + + // Whether the most recent generation attempt failed (a marker newer than + // the last successful generation). The briefing keeps its last good text + // on failure, so this is the only honest signal that a shown briefing is + // held rather than freshly refreshed — and, when there is no last good + // text, that the empty state should read "couldn't generate" with a retry + // rather than the generic "no briefing yet". + const briefingFailure = await readBriefingFailure({ + userId, + since: cachedAt, + }); + // A `briefing-ungrounded` marker is NOT a failure: the generation + // succeeded but the grounding gate withheld the briefing (marker written + // after the cache write, so it survives the `since` filter). It is the + // persistent omission signal — the card's "briefing withheld" state used + // to exist only on the transient POST response, so a nightly strip read as + // a silent generic empty card. + const briefingOmittedReason: "ungrounded" | null = + briefingFailure?.reason === "briefing-ungrounded" ? "ungrounded" : null; + const generationFailed = + briefingFailure !== null && briefingOmittedReason === null; + // The failure class lets the empty state point its hint at the right + // lever (raise the response timeout vs re-check the provider). + const generationFailureClass = generationFailed + ? (briefingFailure?.failureClass ?? null) + : null; + return { + hasProvider, + generationFailed, + generationFailureClass, + briefingOmittedReason, + }; +} + +export async function readAdvisorBriefing(args: { + request: NextRequest; + userId: string; + userLocale: string | null; +}) { + const { request, userId } = args; + const dbUser = await prisma.user.findUnique({ + where: { id: userId }, + select: { + insightsCachedAt: true, + ...INSIGHTS_CACHE_SELECT, + insightsCachedLocale: true, + locale: true, + timezone: true, + }, + }); + const cachedText = openInsightsCachedText(dbUser); + + // The locale the caller is actually reading (cookie / Accept-Language + // fall-back when `User.locale` is unset), validated against the shipped + // list — the same convention the nightly's `normalizeLocale` and the status + // routes follow. Collapsing a NULL `User.locale` to German filled the wrong + // cache family for a client reading the app in English. + const readerLocale = normalizeLocale( + await resolveServerLocale({ + request, + userLocale: dbUser?.locale ?? args.userLocale ?? null, + }), + ); + + // The cache is one slot per user. Text written in another language is + // never the reader's briefing, however fresh: treat the slot as empty for + // prose, and re-warm in the reader's language below. An untagged row + // predates the tag and is served as before. + const languageMismatch = + dbUser?.insightsCachedLocale != null && + dbUser.insightsCachedLocale !== readerLocale; + + const cachedAt = dbUser?.insightsCachedAt ?? null; + const timezone = dbUser?.timezone ?? DEFAULT_TIMEZONE; + // Fresh means written today on the reader's calendar, by the moment the + // text was generated: `insightsCachedAt` also moves when an unchanged + // warm only re-stamps yesterday's text. + const generatedToday = generatedOnLocalToday( + readBriefingGeneratedAt(cachedText), + timezone, + new Date(), + ); + const isFresh = + !languageMismatch && + cachedAt !== null && + generatedToday && + Date.now() - cachedAt.getTime() < BRIEFING_FRESH_MS; + + const signals = await readGenerationSignals(userId, cachedAt); + const { hasProvider } = signals; + + // Never block on the provider. Warm out of band only when the cached + // briefing is stale / missing AND a provider is configured (a + // provider-less account costs one cheap chain-resolve and shows the + // empty / connect-AI state instead of a wasted enqueue). + let revalidating = false; + if (!isFresh && hasProvider) { + void enqueueForceWarm({ userId, locale: readerLocale }); + revalidating = true; + } + + if (cachedText && !languageMismatch) { + try { + const cached = JSON.parse(cachedText); + const legacyPayload = isLegacyInsightPayload(cached); + annotate({ + action: { name: "insights.generate.read" }, + meta: { cached: true, legacyPayload, revalidating, hasProvider }, + }); + return { + // The briefing as every surface serves it: today's text only, its + // signals measured today, its deltas at display precision. + insights: await insightsPayloadForToday(cached, { + userId, + timezone, + language: readerLocale, + }), + cached: true, + cachedAt, + legacyPayload, + // Honest stale-serve marker: true while the out-of-band warm is in + // flight, so the client can poll (bounded) until the fresh briefing + // lands instead of sitting on the stale payload for the rest of the + // session. + revalidating, + // False when no AI provider is configured anywhere: the served + // briefing can never refresh, so the UI pairs its age with a + // connect-provider affordance. + hasProvider, + // True when the last generation attempt failed (held text). + generationFailed: signals.generationFailed, + // Coarse failure class for the empty-state hint (null when the last + // attempt succeeded). + generationFailureClass: signals.generationFailureClass, + // Persistent grounding-omission signal (see above), so a + // nightly-stripped briefing renders "withheld", not "no briefing + // yet". + briefingOmittedReason: signals.briefingOmittedReason, + }; + } catch { + // Invalid cache row — fall through to the empty payload below. The + // warm enqueue above (when a provider exists) repairs it for the next + // read. + } + } + + annotate({ + action: { name: "insights.generate.read" }, + meta: { + cached: false, + revalidating, + hasProvider, + languageMismatch, + readerLocale, + }, + }); + return { + insights: null, + cached: false, + legacyPayload: false, + revalidating, + hasProvider, + // No last-good text AND the last attempt failed: the UI shows a + // "couldn't generate" empty state with a retry instead of the generic + // one. + generationFailed: signals.generationFailed, + generationFailureClass: signals.generationFailureClass, + briefingOmittedReason: signals.briefingOmittedReason, + }; +} diff --git a/src/lib/insights/comprehensive-overview/build.ts b/src/lib/insights/comprehensive-overview/build.ts new file mode 100644 index 000000000..a96e8b751 --- /dev/null +++ b/src/lib/insights/comprehensive-overview/build.ts @@ -0,0 +1,198 @@ +/** + * The Insights overview's response body, computed from the record. + * + * SQL-side aggregates served by `buildComprehensiveAggregate` replace the + * 100k-row-in-JS aggregation the route once ran; the medication-compliance + * block keeps its bounded Prisma reads (medications + intake events). No + * provider is called anywhere on the path: `hasProvider` is the presence + * probe only. + * + * Every day-bucketing pass (resting-pulse proxy, medication continuity, BP + * pairing) shares the one per-user tz resolved here. + */ +import { prisma } from "@/lib/db"; +import { probeProviderPresence } from "@/lib/ai/provider"; +import type { DataSummary } from "@/lib/analytics/trends"; +import { summarize } from "@/lib/analytics/trends"; +import { getBpTargets } from "@/lib/analytics/bp-targets"; +import { computeBpInTargetPct } from "@/lib/analytics/bp-in-target"; +import { + classifyBMI, + classifyBP, + generateAlerts, +} from "@/lib/analytics/classifications"; +import { annotate } from "@/lib/logging/context"; +import { buildComprehensiveAggregate } from "@/lib/insights/comprehensive-aggregator"; +import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; + +import { overviewCorrelations } from "./correlations"; +import { + bpMedicationContinuity, + readMedicationCompliance, +} from "./medication-compliance"; +import { readOverviewMoodSeries } from "./mood-series"; +import { readRestingPulseDaily } from "./resting-pulse-daily"; + +/** Who the overview is computed for: the record, with its zone. */ +export interface OverviewRecord { + id: string; + timezone?: string | null; +} + +export type ComprehensiveOverviewBody = Awaited< + ReturnType +>; + +/** BMI and the 30-day BP classification against the record's profile. */ +function classifyProfile( + summaries: Record, + dbUser: { heightCm: number | null; dateOfBirth: Date | null } | null, +) { + let bmi: number | null = null; + let bmiClassification = null; + if (dbUser?.heightCm && summaries.WEIGHT?.latest) { + const heightM = dbUser.heightCm / 100; + bmi = Math.round((summaries.WEIGHT.latest / (heightM * heightM)) * 10) / 10; + bmiClassification = classifyBMI(bmi); + } + + let bpClassification = null; + const bpTargets = getBpTargets(dbUser?.dateOfBirth ?? null); + if ( + summaries.BLOOD_PRESSURE_SYS?.avg30 && + summaries.BLOOD_PRESSURE_DIA?.avg30 + ) { + bpClassification = classifyBP( + summaries.BLOOD_PRESSURE_SYS.avg30, + summaries.BLOOD_PRESSURE_DIA.avg30, + ); + } + return { bmi, bmiClassification, bpClassification, bpTargets }; +} + +export async function buildComprehensiveResponse(user: OverviewRecord) { + const userId = user.id; + const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); + const userTz = user.timezone || DEFAULT_TIMEZONE; + + // Height + DOB drive BMI + BP targets. + const dbUser = await prisma.user.findUnique({ + where: { id: userId }, + select: { + heightCm: true, + dateOfBirth: true, + }, + }); + + // SQL-side rollup: per-type DataSummary identical to the JS `summarize()` + // semantics over the 90-day window, plus the bounded raw rows the BP target + // adherence pairing needs, plus daily-mean buckets for the correlations. + const aggregate = await buildComprehensiveAggregate(userId); + + const { dailyMoodEntries, moodDataPoints, moodEntryCount } = + await readOverviewMoodSeries(userId, ninetyDaysAgo); + const moodSummary = + moodDataPoints.length > 0 ? summarize(moodDataPoints) : null; + + const summaries: Record = aggregate.summaries; + const { bmi, bmiClassification, bpClassification, bpTargets } = + classifyProfile(summaries, dbUser); + + // BP target adherence over the trailing 90 days, through the canonical + // `computeBpInTargetPct` so this figure shares ONE definition with the + // dashboard tile, the insight cards, the AI snapshot and the targets + // endpoint. The helper pairs sys+dia on the 5-minute window with the + // same-local-day fallback (keyed on the user's OWN tz) and keeps the + // ESH-2023 band + 90/50 hypotension floor. Window is the aggregator's + // 90-day raw pull — a "recent control" figure, not the all-time headline. + let bpPctInTarget: number | null = null; + if (bpTargets) { + bpPctInTarget = + computeBpInTargetPct( + aggregate.bpRawRows.sys, + aggregate.bpRawRows.dia, + bpTargets, + userTz, + )?.pct ?? null; + } + + const restingPulseDaily = await readRestingPulseDaily( + userId, + ninetyDaysAgo, + userTz, + ); + const sysDaily = aggregate.dailyByType.BLOOD_PRESSURE_SYS ?? []; + const correlations = overviewCorrelations({ + aggregate, + dailyMoodEntries, + restingPulseDaily, + userTz, + }); + + const { medications, medCompliance, bpMedications, bpMedicationEvents } = + await readMedicationCompliance({ + userId, + since: ninetyDaysAgo, + timezone: user.timezone || DEFAULT_TIMEZONE, + }); + const { bpMedicationCorrelation, bpMedicationScatterData } = + bpMedicationContinuity({ bpMedications, bpMedicationEvents, sysDaily }); + + const alerts = generateAlerts({ + bmi, + bpAvgSys: summaries.BLOOD_PRESSURE_SYS?.avg30 ?? null, + bpAvgDia: summaries.BLOOD_PRESSURE_DIA?.avg30 ?? null, + bpPctInTarget, + weightSlope30: summaries.WEIGHT?.slope30?.slope ?? null, + pulseAvg30: summaries.PULSE?.avg30 ?? null, + pulseAnomalyCount: summaries.PULSE?.anomalyCount ?? 0, + medications: medCompliance.map((m) => ({ + name: m.name, + compliance7: m.compliance7, + compliance30: m.compliance30, + })), + }); + + // Data span — derived from the earliest 90-day measurement. + const dataSpanDays = aggregate.firstMeasurementAt + ? Math.ceil( + (Date.now() - aggregate.firstMeasurementAt.getTime()) / + (24 * 60 * 60 * 1000), + ) + : 0; + + annotate({ + action: { name: "insights.comprehensive" }, + meta: { + totalMeasurements: aggregate.totalMeasurements, + moodEntries: moodEntryCount, + medications: medications.length, + }, + }); + + return { + summaries, + bmi, + bmiClassification, + bpClassification, + bpPctInTarget, + bpTargets, + weightBpCorrelation: correlations.weightBpCorrelation, + scatterData: correlations.scatterData, + bpMedicationCorrelation, + bpMedicationScatterData, + moodSummary, + moodBpCorrelation: correlations.moodBpCorrelation, + moodBpScatterData: correlations.moodBpScatterData, + moodWeightCorrelation: correlations.moodWeightCorrelation, + moodWeightScatterData: correlations.moodWeightScatterData, + moodPulseCorrelation: correlations.moodPulseCorrelation, + moodPulseScatterData: correlations.moodPulseScatterData, + medications: medCompliance, + alerts, + // Presence only: no client is built and no token is refreshed on a read. + hasProvider: await probeProviderPresence(userId), + dataSpanDays, + totalMeasurements: aggregate.totalMeasurements, + }; +} diff --git a/src/lib/insights/comprehensive-overview/correlations.ts b/src/lib/insights/comprehensive-overview/correlations.ts new file mode 100644 index 000000000..ab9f6ce97 --- /dev/null +++ b/src/lib/insights/comprehensive-overview/correlations.ts @@ -0,0 +1,78 @@ +/** + * The overview's correlations: weight × systolic, and mood against + * systolic, weight and resting pulse. Pure: every series is already read. + */ +import { + significantPearsonCorrelation, + type PairedPoint, +} from "@/lib/analytics/correlations"; +import type { ComprehensiveAggregate } from "@/lib/insights/comprehensive-aggregator"; + +import { + deriveDailyLocalMeans, + joinDailyByDay, + pairMoodWithDaily, + type DailyValue, +} from "./daily-series"; + +export function overviewCorrelations(args: { + aggregate: ComprehensiveAggregate; + dailyMoodEntries: DailyValue[]; + restingPulseDaily: DailyValue[]; + userTz: string; +}) { + const { aggregate, dailyMoodEntries, restingPulseDaily, userTz } = args; + + // Weight × Sys BP, daily-key pairing. Both sides come from the UTC + // `dailyByType` series, so they share one UTC day-key space and are + // internally consistent; re-keying it to local migrates with the rollup + // tier. + const weightDaily = aggregate.dailyByType.WEIGHT ?? []; + const sysDaily = aggregate.dailyByType.BLOOD_PRESSURE_SYS ?? []; + const weightBpPairs: PairedPoint[] = joinDailyByDay(weightDaily, sysDaily); + const weightBpCorrelation = significantPearsonCorrelation(weightBpPairs); + const scatterData = weightBpPairs.map((p) => ({ + weight: p.a, + sysBP: p.b, + })); + + // Mood × {sys BP, weight, pulse}. The mood series is LOCAL-day-keyed and + // `pairMoodWithDaily` joins on an EXACT YYYY-MM-DD key, so the partners + // are exact local-day means from the raw rows already on the request path + // (canonical-source deduped in the aggregator), NOT the UTC `dailyByType` — + // a UTC-day MEAN cannot be relabeled into a local-day mean. + const sysDailyLocal = deriveDailyLocalMeans(aggregate.bpRawRows.sys, userTz); + const weightDailyLocal = deriveDailyLocalMeans( + aggregate.weightRawRows, + userTz, + ); + + const moodBpPairs = pairMoodWithDaily(dailyMoodEntries, sysDailyLocal); + const moodBpCorrelation = significantPearsonCorrelation(moodBpPairs); + const moodBpScatterData = moodBpPairs.map((p) => ({ mood: p.a, sysBP: p.b })); + + const moodWeightPairs = pairMoodWithDaily(dailyMoodEntries, weightDailyLocal); + const moodWeightCorrelation = significantPearsonCorrelation(moodWeightPairs); + const moodWeightScatterData = moodWeightPairs.map((p) => ({ + mood: p.a, + weight: p.b, + })); + + const moodPulsePairs = pairMoodWithDaily(dailyMoodEntries, restingPulseDaily); + const moodPulseCorrelation = significantPearsonCorrelation(moodPulsePairs); + const moodPulseScatterData = moodPulsePairs.map((p) => ({ + mood: p.a, + pulse: p.b, + })); + + return { + weightBpCorrelation, + scatterData, + moodBpCorrelation, + moodBpScatterData, + moodWeightCorrelation, + moodWeightScatterData, + moodPulseCorrelation, + moodPulseScatterData, + }; +} diff --git a/src/lib/insights/comprehensive-overview/daily-series.ts b/src/lib/insights/comprehensive-overview/daily-series.ts new file mode 100644 index 000000000..e80d9ba71 --- /dev/null +++ b/src/lib/insights/comprehensive-overview/daily-series.ts @@ -0,0 +1,110 @@ +/** + * Day-keyed series helpers for the Insights overview's correlations: local + * day means from raw rows, and the two joins that pair one daily series with + * another. + */ +import type { PairedPoint } from "@/lib/analytics/correlations"; +import { userDayKey } from "@/lib/tz/format"; + +export interface DailyValue { + day: string; + value: number; +} + +/** + * Derive exact local-day mean series from raw measurement rows, keyed by the + * user's timezone via `userDayKey`. The mood series is local-day-keyed, and + * `pairMoodWithDaily` joins on an exact YYYY-MM-DD key, so its metric + * partners must be local-day-keyed too. A UTC-day mean (from `dailyByType`) + * cannot be relabeled into a local-day mean, so the mean is recomputed here + * from raw rows already on the request path. `userDayKey` formats via a + * memoised `Intl.DateTimeFormat` — no offset arithmetic — so DST-transition + * days (23 h / 25 h) bucket correctly and the result is host-TZ-independent + * (CI runs UTC). `round2` mirrors the aggregator's `ROUND(AVG, 2)` so + * correlation thresholds don't drift. Returns `{ day, value }[]` sorted ASC + * by day, the shape `pairMoodWithDaily` expects. + */ +export function deriveDailyLocalMeans( + rows: Array<{ measuredAt: Date; value: number }>, + tz: string, +): DailyValue[] { + const byDay = new Map(); + for (const r of rows) { + const key = userDayKey(r.measuredAt, tz); + const cur = byDay.get(key) ?? { sum: 0, count: 0 }; + cur.sum += r.value; + cur.count += 1; + byDay.set(key, cur); + } + return Array.from(byDay.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([day, stats]) => ({ + day, + value: Math.round((stats.sum / stats.count) * 100) / 100, + })); +} + +/** + * Daily-key inner join between two daily-aggregated series. Returns + * `PairedPoint` shapes compatible with `significantPearsonCorrelation`. + * + * Inputs are sorted ASC by `day` (the aggregator's ORDER BY + * guarantees that). The walk is O(n + m) merge-join style instead of + * Map.lookup-per-element so it scales with the worst-case 90 buckets + * x 90 buckets case without surprises. + */ +export function joinDailyByDay( + a: DailyValue[], + b: DailyValue[], +): PairedPoint[] { + const pairs: PairedPoint[] = []; + let i = 0; + let j = 0; + while (i < a.length && j < b.length) { + const ai = a[i]; + const bj = b[j]; + if (ai.day === bj.day) { + pairs.push({ + a: ai.value, + b: bj.value, + date: new Date(`${ai.day}T12:00:00.000Z`), + }); + i += 1; + j += 1; + } else if (ai.day < bj.day) { + i += 1; + } else { + j += 1; + } + } + return pairs; +} + +/** + * Pair the daily mood series (left side) with a daily-aggregated + * metric (right side). Matches the legacy `buildMoodMetricPairs` + * helper byte-for-byte: same daily YYYY-MM-DD key, same + * `T12:00:00.000Z` anchor for the `date` field. + */ +export function pairMoodWithDaily( + dailyMood: DailyValue[], + dailyMetric: DailyValue[], +): PairedPoint[] { + if (dailyMood.length === 0 || dailyMetric.length === 0) return []; + const metricByDay = new Map(); + for (const m of dailyMetric) { + metricByDay.set(m.day, m.value); + } + const pairs: PairedPoint[] = []; + for (const mood of dailyMood) { + const metric = metricByDay.get(mood.day); + if (metric !== undefined) { + pairs.push({ + a: mood.value, + b: metric, + date: new Date(`${mood.day}T12:00:00.000Z`), + }); + } + } + return pairs; +} diff --git a/src/lib/insights/comprehensive-overview/medication-compliance.ts b/src/lib/insights/comprehensive-overview/medication-compliance.ts new file mode 100644 index 000000000..43a4567ab --- /dev/null +++ b/src/lib/insights/comprehensive-overview/medication-compliance.ts @@ -0,0 +1,235 @@ +/** + * The overview's medication block: per-medication compliance over 7 and 30 + * days, and the blood-pressure medication continuity × systolic correlation + * that pairs the BP medications' intake days with the daily systolic means. + */ +import { prisma } from "@/lib/db"; +import { + buildComplianceMedicationContext, + COURSES_COMPLIANCE_SELECT, + calculateCompliance, + expectsDoses, + lastNonSkippedTakenAt, + SCHEDULE_COMPLIANCE_SELECT, +} from "@/lib/analytics/compliance"; +import { significantPearsonCorrelation } from "@/lib/analytics/correlations"; +import { resolveMedicationCategories } from "@/lib/medication-category"; +import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking"; +import { dayKeyAsUtcMidnight } from "@/lib/tz/date-only"; + +import type { DailyValue } from "./daily-series"; + +interface BpIntakeEvent { + scheduledFor: Date; + takenAt: Date | null; + skipped: boolean; +} + +/** Read the tracked scheduled medications with what compliance needs. */ +async function readScheduledMedications(userId: string) { + const medicationRows = await prisma.medication.findMany({ + // As-needed (PRN) medications never surface a compliance rate (no + // expected doses). + where: { userId, active: true, asNeeded: false, ...TRACKED_INTAKE_WHERE }, + include: { + // Schedules through the shared compliance select so the configured + // per-dose windows reach this surface like every other. + schedules: { select: SCHEDULE_COMPLIANCE_SELECT }, + // Archived schedule eras for era-aware compliance. + scheduleRevisions: { orderBy: { validFrom: "asc" } }, + // Pause eras so paused days drop out of the denominator. + pauseEras: { select: { pausedAt: true, resumedAt: true } }, + // The courses, so a gap between two expects nothing (#1024). + courses: COURSES_COMPLIANCE_SELECT, + }, + }); + // A medication with no schedule expects no dose, and `calculateCompliance` + // answers 100 % for that case. The `asNeeded: false` filter above catches + // only the PRN arm; the empty schedule is reachable on a scheduled + // medication and would otherwise report perfect adherence. + return medicationRows.filter(expectsDoses); +} + +/** + * Per-medication compliance, plus the intake events of the blood-pressure + * medications for the continuity correlation. + */ +export async function readMedicationCompliance(args: { + userId: string; + since: Date; + timezone: string; +}) { + const { userId, since, timezone } = args; + const medications = await readScheduledMedications(userId); + + const resolvedCategories = await resolveMedicationCategories( + medications.map((m) => m.id), + ); + const categoryMap = Object.fromEntries( + Object.entries(resolvedCategories).map(([id, r]) => [id, r.category]), + ); + + const medCompliance = []; + const bpMedicationEvents: BpIntakeEvent[] = []; + const bpMedications = medications.filter( + (med) => (categoryMap[med.id] ?? "OTHER") === "BLOOD_PRESSURE", + ); + // Single round-trip for all medications instead of N+1: one query keyed + // on `medicationId IN (...)`, then group in memory. + const allEvents = medications.length + ? await prisma.medicationIntakeEvent.findMany({ + where: { + medicationId: { in: medications.map((m) => m.id) }, + userId, + // Tombstoned rows are excluded. + deletedAt: null, + scheduledFor: { gte: since }, + }, + orderBy: { scheduledFor: "desc" }, + }) + : []; + + const eventsByMed = new Map(); + for (const ev of allEvents) { + const list = eventsByMed.get(ev.medicationId); + if (list) list.push(ev); + else eventsByMed.set(ev.medicationId, [ev]); + } + + for (const med of medications) { + const events = eventsByMed.get(med.id) ?? []; + const mapped = events.map((e) => ({ + takenAt: e.takenAt, + skipped: e.skipped, + scheduledFor: e.scheduledFor, + })); + // Engine-routed denominator. + const medicationContext = buildComplianceMedicationContext( + med, + lastNonSkippedTakenAt(mapped), + timezone, + ); + const c7 = calculateCompliance(mapped, med.schedules, 7, med.createdAt, { + medicationContext, + }); + const c30 = calculateCompliance(mapped, med.schedules, 30, med.createdAt, { + medicationContext, + }); + + medCompliance.push({ + id: med.id, + name: med.name, + dose: med.dose, + category: categoryMap[med.id] ?? "OTHER", + // The label of a custom category; null for a built-in (#1041). + categoryLabel: resolvedCategories[med.id]?.categoryLabel ?? null, + compliance7: c7.rate, + compliance30: c30.rate, + streak: c7.streak, + taken7: c7.taken, + skipped7: c7.skipped, + missed7: c7.missed, + }); + + if ((categoryMap[med.id] ?? "OTHER") === "BLOOD_PRESSURE") { + bpMedicationEvents.push( + ...events.map((e) => ({ + scheduledFor: e.scheduledFor, + takenAt: e.takenAt, + skipped: e.skipped, + })), + ); + } + } + + return { medications, medCompliance, bpMedications, bpMedicationEvents }; +} + +/** + * Correlation: BP medication continuity × systolic BP. + * + * Uses the same daily BP_SYS series the aggregator pre-computed — no second + * round-trip. The intake-side aggregation matches the legacy code: + * continuity = min(1, taken / expectedPerDay), pairing every day that had a + * sys reading. + */ +export function bpMedicationContinuity(args: { + bpMedications: Array<{ schedules: unknown[] }>; + bpMedicationEvents: BpIntakeEvent[]; + sysDaily: DailyValue[]; +}) { + const { bpMedications, bpMedicationEvents, sysDaily } = args; + let bpMedicationCorrelation: { + r: number; + strength: string; + n: number; + medicationCount: number; + } | null = null; + const bpMedicationScatterData: Array<{ + continuityPct: number; + sysBP: number; + }> = []; + + const expectedBpIntakesPerDay = bpMedications.reduce( + (sum, med) => sum + med.schedules.length, + 0, + ); + + if (expectedBpIntakesPerDay > 0) { + // Daily systolic means (already keyed YYYY-MM-DD) from the SQL pass. + // `sysDaily` (UTC `date_trunc` from the aggregator's `dailyByType`) and + // `takenByDay` below (intake `scheduledFor` truncated to UTC) both live + // in the SAME UTC day-key space and are internally consistent with each + // other, so this join stays UTC. Only the mood pairings moved to a + // separately derived local-day series (their partner, the mood series, + // is local-day-keyed). This correlation migrates to local when the + // measurement-rollup tier buckets per-user tz; re-keying it here without + // the local-bucketed rollup would mean per-request raw re-aggregation. + const sysByDay = new Map(); + for (const row of sysDaily) { + sysByDay.set(row.day, row.value); + } + + const takenByDay = new Map(); + // Days that carry an intake record of ANY kind. A logged skip is a real + // zero and belongs in the series; a day the person never touched is not a + // zero, it is nothing, and pairing it against that day's systolic reading + // invented a data point they never produced. Absence is not zero, here as + // everywhere else in this project, so the unlogged day drops out of both + // the scatter and the coefficient rather than dragging continuity down. + const loggedDays = new Set(); + for (const event of bpMedicationEvents) { + // eslint-disable-next-line healthlog/no-utc-day-key -- UTC by design: pairs intake days with the DAY rollup buckets in sysByDay, which are UTC days + const dayKey = event.scheduledFor.toISOString().slice(0, 10); + loggedDays.add(dayKey); + if (event.skipped || !event.takenAt) continue; + takenByDay.set(dayKey, (takenByDay.get(dayKey) ?? 0) + 1); + } + + const pairs: Array<{ a: number; b: number; date: Date }> = []; + for (const [dayKey, avgSys] of sysByDay.entries()) { + if (!loggedDays.has(dayKey)) continue; + const taken = takenByDay.get(dayKey) ?? 0; + const continuity = Math.min(1, taken / expectedBpIntakesPerDay); + pairs.push({ + a: continuity, + b: avgSys, + date: dayKeyAsUtcMidnight(dayKey), + }); + bpMedicationScatterData.push({ + continuityPct: Math.round(continuity * 100), + sysBP: Math.round(avgSys * 10) / 10, + }); + } + + const corr = significantPearsonCorrelation(pairs); + if (corr) { + bpMedicationCorrelation = { + ...corr, + medicationCount: bpMedications.length, + }; + } + } + + return { bpMedicationCorrelation, bpMedicationScatterData }; +} diff --git a/src/lib/insights/comprehensive-overview/mood-series.ts b/src/lib/insights/comprehensive-overview/mood-series.ts new file mode 100644 index 000000000..40efa5afd --- /dev/null +++ b/src/lib/insights/comprehensive-overview/mood-series.ts @@ -0,0 +1,92 @@ +/** + * The overview's 90-day mood series, read from the mood-rollup DAY tier. + * + * The two consumers downstream — `dailyMoodEntries` for the mood × metric + * correlations and the mood summary for the dashboard mood block — read at a + * per-day resolution, which is precisely the shape the rollup writer emits, + * so the tier replaces an unbounded `prisma.moodEntry.findMany`. + * + * Coverage-fallback: when the user has mood entries but zero rollup rows yet + * (legacy account before the boot-time backfill has caught up), fall back to + * a bounded 90-day raw walk once and fire the warm-up so the next request + * lands on the rollup tier. Same posture as `/api/mood/analytics`. + * + * The mood rollup writer mints `bucketStart` from the canonical local + * `MoodEntry.date` label at UTC midnight, so `.toISOString().slice(0, 10)` + * recovers the user's LOCAL calendar day — byte-identical to the live + * fallback's `entry.date`. Both mood paths therefore emit local-day keys, and + * the mood × metric pairing partners (sys BP, weight, resting pulse) are + * derived in the user's tz so both join sides share ONE local-day key space. + * The measurement-rollup tier itself still buckets UTC (`dailyByType`); the + * two internally-consistent UTC joins (weight × BP, BP-med continuity) are + * left on it deliberately (see their call sites in `build.ts`). + */ +import { prisma } from "@/lib/db"; +import type { DataPoint } from "@/lib/analytics/trends"; +import { + ensureUserMoodRollupsFresh, + readMoodDayRollups, +} from "@/lib/rollups/mood-rollups"; +import { dateOnlyKey } from "@/lib/tz/date-only"; + +import type { DailyValue } from "./daily-series"; + +export interface OverviewMoodSeries { + dailyMoodEntries: DailyValue[]; + moodDataPoints: DataPoint[]; + moodEntryCount: number; +} + +export async function readOverviewMoodSeries( + userId: string, + since: Date, +): Promise { + void ensureUserMoodRollupsFresh(userId); + const moodRollupDayRows = await readMoodDayRollups(userId, since); + if (moodRollupDayRows.length > 0) { + return { + dailyMoodEntries: moodRollupDayRows.map((r) => ({ + day: dateOnlyKey(r.bucketStart), + value: Math.round(r.mean * 100) / 100, + })), + moodDataPoints: moodRollupDayRows.map((r) => ({ + date: r.bucketStart, + value: r.mean, + })), + moodEntryCount: moodRollupDayRows.reduce((s, r) => s + r.count, 0), + }; + } + // Coverage-fallback: legacy account with mood entries but no + // rollup coverage yet. Bounded by the 90-day window so the walk is + // capped even when the rollup miss happens. + const moodEntries = await prisma.moodEntry.findMany({ + // Tombstoned rows are excluded. + where: { userId, deletedAt: null, moodLoggedAt: { gte: since } }, + orderBy: { moodLoggedAt: "asc" }, + select: { date: true, score: true, moodLoggedAt: true }, + }); + const moodByDay = new Map(); + for (const entry of moodEntries) { + const current = moodByDay.get(entry.date) ?? { sum: 0, count: 0 }; + current.sum += entry.score; + current.count += 1; + moodByDay.set(entry.date, current); + } + const dailyMoodEntries = Array.from(moodByDay.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([day, stats]) => ({ + day, + value: Math.round((stats.sum / stats.count) * 100) / 100, + })); + return { + dailyMoodEntries, + // Per-day means feed `summarize()` so the live-fallback path shares the + // rollup-tier semantic (one DataPoint per day, the daily mean), the same + // reading mood/analytics uses. + moodDataPoints: dailyMoodEntries.map((e) => ({ + date: new Date(`${e.day}T12:00:00.000Z`), + value: e.value, + })), + moodEntryCount: moodEntries.length, + }; +} diff --git a/src/lib/insights/comprehensive-overview/overview.ts b/src/lib/insights/comprehensive-overview/overview.ts new file mode 100644 index 000000000..15cc843e6 --- /dev/null +++ b/src/lib/insights/comprehensive-overview/overview.ts @@ -0,0 +1,49 @@ +/** + * The Insights overview read: the computed body through the analytics + * cache. The route resolves the per-request AI state beside it, which is + * never cached with the body. + */ +import { annotate } from "@/lib/logging/context"; +import { + cachedSwrWithMeta, + caches, + type ServerCache, +} from "@/lib/cache/server-cache"; + +import { + buildComprehensiveResponse, + type ComprehensiveOverviewBody, + type OverviewRecord, +} from "./build"; + +export async function readComprehensiveOverview( + user: OverviewRecord, +): Promise<{ body: ComprehensiveOverviewBody; revalidating: boolean }> { + // Read-through the analytics cache keyed on (userId, "comprehensive"). The + // /insights page mount routinely fans out to this endpoint alongside the + // Coach drawer + the recommendations grid; the 60s TTL converts every + // duplicate mount within the window to a Map lookup. Invalidation is + // handled by `invalidateUserMeasurements`, which marks every key under the + // `${userId}|` prefix stale (see `lib/cache/invalidate.ts`). + // + // Stale-while-revalidate: this is the single heaviest SQL-side aggregation + // on the /insights mount; serving the prior body instantly while ONE + // background recompute refreshes it keeps the page interactive across the + // bucket's 10-minute stale window (a measurement sync used to bust the + // entry into a blocking cold rebuild on the very next mount, all day). The + // truly-cold first read of the day still computes inline — there is no + // prior body to serve — but it no longer recurs per visit. + // + // The `revalidating` flag marks a stale-served body honestly so the client + // can poll (bounded) until the background rebuild lands — mirroring the + // `/api/insights/generate` GET contract. It rides on the response, never + // inside the cached body, so a warmed entry doesn't freeze the flag. + const { value: body, outcome } = await cachedSwrWithMeta( + caches.analytics as ServerCache, + `${user.id}|comprehensive`, + () => buildComprehensiveResponse(user), + annotate, + ); + + return { body, revalidating: outcome === "stale" }; +} diff --git a/src/lib/insights/comprehensive-overview/resting-pulse-daily.ts b/src/lib/insights/comprehensive-overview/resting-pulse-daily.ts new file mode 100644 index 000000000..acb54fc0d --- /dev/null +++ b/src/lib/insights/comprehensive-overview/resting-pulse-daily.ts @@ -0,0 +1,65 @@ +/** + * The overview's daily resting-pulse series, the mood × pulse correlation's + * partner. + * + * The correlation is labelled "Resting pulse (bpm)", so it is scored against + * the RESTING series, not the raw PULSE day-mean: the aggregator's + * `dailyByType.PULSE` is a per-day AVG that folds in workout HR for + * Apple-Health users and inflates the "resting" figure. The clean + * RESTING_HEART_RATE rows win; only without them does the low-percentile + * daily proxy over raw pulse stand in. The proxy folds in Postgres (#1023): + * it needs one percentile per day, and reading the raw samples to get it + * walked 130 000 rows per page view on a watch that records once a minute. + * + * Keyed on `userDayKey(d, userTz)`: its only pairing partner is the mood + * series, which is local-day-keyed, and a UTC key here would offset every + * evening logger west of UTC by one day and sell a lag-1 correlation as + * same-day. Unlike the mood side, this series builds from raw rows, so it + * re-keys directly with no rollup-tier change. + */ +import { prisma } from "@/lib/db"; +import { resolveRestingPulseSeries } from "@/lib/analytics/resting-pulse"; +import { readRestingPulseProxy } from "@/lib/analytics/resting-pulse-read"; +import { userDayKey } from "@/lib/tz/format"; + +import type { DailyValue } from "./daily-series"; + +export async function readRestingPulseDaily( + userId: string, + since: Date, + userTz: string, +): Promise { + const restingPulseRows = await prisma.measurement.findMany({ + where: { + userId, + type: "RESTING_HEART_RATE", + deletedAt: null, + measuredAt: { gte: since }, + }, + select: { measuredAt: true, value: true }, + }); + const localDayKey = (d: Date) => userDayKey(d, userTz); + const restingPulseSeries = + restingPulseRows.length > 0 + ? resolveRestingPulseSeries({ + restingSamples: restingPulseRows, + pulseSamples: [], + dayKeyOf: localDayKey, + }).series + : await readRestingPulseProxy({ + userId, + since, + timeZone: userTz, + }); + const restingByDay = new Map(); + for (const s of restingPulseSeries) { + const key = localDayKey(s.measuredAt); + const cur = restingByDay.get(key) ?? { sum: 0, count: 0 }; + cur.sum += s.value; + cur.count += 1; + restingByDay.set(key, cur); + } + return Array.from(restingByDay.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([day, stats]) => ({ day, value: stats.sum / stats.count })); +} diff --git a/src/lib/measurements/list/measurement-list.ts b/src/lib/measurements/list/measurement-list.ts index 57e0535fd..2aea645b1 100644 --- a/src/lib/measurements/list/measurement-list.ts +++ b/src/lib/measurements/list/measurement-list.ts @@ -28,7 +28,38 @@ import { import { dayGroupedListResponse } from "@/lib/measurements/list/day-grouped"; import { sleepListResponse } from "@/lib/measurements/list/sleep-list"; import { resolveStoredTimezone, resolveUserTimezone } from "@/lib/tz/resolver"; -import type { MeasurementType } from "@/generated/prisma/client"; +import type { MeasurementType, Prisma } from "@/generated/prisma/client"; + +/** + * The columns a measurement row answers with (`MeasurementResource`), plus + * the sealed note `shapeMeasurementNotes` decrypts onto `notes` and drops. + * Named column by column so a column added to the model reaches the wire + * only when someone adds it here. + */ +const MEASUREMENT_WIRE_SELECT = { + id: true, + userId: true, + type: true, + value: true, + valueMin: true, + valueMax: true, + unit: true, + source: true, + measuredAt: true, + notes: true, + notesEncrypted: true, + externalId: true, + externalSourceVersion: true, + aggregationProvenance: true, + glucoseContext: true, + sleepStage: true, + rhythmClassification: true, + deviceType: true, + syncVersion: true, + deletedAt: true, + createdAt: true, + updatedAt: true, +} as const satisfies Prisma.MeasurementSelect; type ListQuery = z.output; type ListUser = { id: string; timezone: string | null }; @@ -162,13 +193,16 @@ async function drillDownResponse( // returning 422 when a caller asks for more). `limit` is read straight // through because the validator has already clamped it. take: limit, + select: MEASUREMENT_WIRE_SELECT, }); annotate({ action: { name: "measurement.list" }, meta: { total: samples.length, type, dayKey, mode: "drill-down" }, }); return apiSuccess({ - measurements: samples, + // The same row shape as the plain page: the note decrypted onto `notes`, + // the ciphertext never on the wire. + measurements: samples.map(shapeMeasurementNotes), meta: { total: samples.length, limit, offset: 0, dayKey }, }); } diff --git a/src/lib/measurements/series/kinds.ts b/src/lib/measurements/series/kinds.ts new file mode 100644 index 000000000..e9a1f76cc --- /dev/null +++ b/src/lib/measurements/series/kinds.ts @@ -0,0 +1,167 @@ +/** + * The vocabulary of `GET /api/measurements/series`: the camelCase kinds the + * route serves, the stored type and wire unit behind each, the point shape, + * and the thresholds that decide when a sample-dense kind is bucketed. + */ +import { z } from "zod/v4"; + +import type { MeasurementType, SleepStage } from "@/generated/prisma/client"; +import { READING_SPREAD, standardDeviation } from "@/lib/stats/spread"; + +/** + * Sample-dense kinds read raw rows only up to this window. + * A CGM account stores ~288 BLOOD_GLUCOSE rows/day (105k+ rows/year) and + * per-sample PULSE piles up similarly, so the 365 / 3650-day requests + * walked a six-figure row set into JS, mapped it twice, and serialized + * everything. Mirrors the sleep precedent above, but instead of silently + * clamping the window, requests beyond the cap keep their full range and + * are day-bucketed in SQL (one point per day, mean + min/max band) — the + * chart still shows the whole requested history, just at daily grain. + * The short windows users actually browse (7 / 30 / 90 d) stay raw and + * byte-identical. Sparse kinds (weight, BP, …) are untouched. + */ +export const DENSE_SERIES_RAW_WINDOW_DAYS = 90; +/** + * The kinds a device can write at sampling rate. Heart-rate variability and + * blood oxygen joined pulse and glucose once watches and rings began writing + * them every few minutes overnight: their 3650-day "Alle" range read every + * raw row, which is the read this rule exists to stop. + */ +export const DENSE_SERIES_KINDS: ReadonlySet = new Set([ + "glucose", + "pulse", + "heartRateVariability", + "oxygenSaturation", +]); + +/** + * #1023 — inside the raw window, a dense-kind series (pulse, glucose, HRV, + * blood oxygen) with more rows than this is bucketed per local hour in SQL + * instead of sent raw. + * + * A watch that records heart rate once a minute puts 43 000 rows in the + * default 30-day window and 130 000 in 90 days: a 15 MB response the client + * could not draw at that resolution anyway, built from as many objects on the + * server. A CGM reading every five minutes puts 26 000 glucose rows in 90 + * days, the same problem at a smaller scale. Hourly buckets carry the hour's + * mean (pulse also its low/high band, the shape an hourly-bucket import + * already has) and cap the answer at 24 points a day; the statistics stay + * computed over every raw reading. The cap sits far above what sparse sources + * produce (a cuff, fingerstick glucose, hourly buckets over 90 days, a watch + * sampling every five minutes over 30 days), so those series stay raw and + * unchanged. + */ +export const DENSE_SERIES_RAW_ROW_CAP = 10_000; + +export const kindEnum = z.enum([ + "weight", + "bloodPressure", + "pulse", + "bodyFat", + "glucose", + "sleep", + "steps", + "totalBodyWater", + "boneMass", + "oxygenSaturation", + // The iOS app surfaces these as series-capable, but + // the route used to reject the trend/detail view request with a + // 422. The underlying MeasurementType enum already carries each + // value; this list extends the camelCase wire shape so the route + // matches the data it can serve. + "restingHeartRate", + "heartRateVariability", + "vo2Max", +]); + +export type SeriesKind = z.infer; + +export const KIND_TO_TYPE: Record = { + weight: "WEIGHT", + bloodPressure: "BLOOD_PRESSURE_SYS", + pulse: "PULSE", + bodyFat: "BODY_FAT", + glucose: "BLOOD_GLUCOSE", + sleep: "SLEEP_DURATION", + steps: "ACTIVITY_STEPS", + totalBodyWater: "TOTAL_BODY_WATER", + boneMass: "BONE_MASS", + oxygenSaturation: "OXYGEN_SATURATION", + restingHeartRate: "RESTING_HEART_RATE", + heartRateVariability: "HEART_RATE_VARIABILITY", + vo2Max: "VO2_MAX", +}; + +/** + * Explicit per-kind unit token returned at the top level so + * the client never has to infer the value's unit. `bloodPressure` reuses + * the systolic type's unit (mmHg, same for both lines). `sleep` is + * overridden at request time to `"h"` because the route returns per-night + * TIME-ASLEEP in hours rather than the canonical per-stage minutes. + * + * `glucose` is the canonical-stored unit (mg/dL) here; it is + * overridden at request time to the user's `glucoseUnit` preference and the + * point values are converted to match, so this wire DTO reads in the SAME + * unit as the CSV + FHIR exports and the blood-glucose detail page (one + * number, one engine). + */ +export const SERIES_UNIT: Record = { + weight: "kg", + bloodPressure: "mmHg", + pulse: "bpm", + bodyFat: "%", + glucose: "mg/dL", + sleep: "h", + steps: "steps", + totalBodyWater: "kg", + boneMass: "kg", + oxygenSaturation: "%", + restingHeartRate: "bpm", + heartRateVariability: "ms", + vo2Max: "mL/(kg·min)", +}; + +export interface SeriesPoint { + id: string; + at: string; + value: number; + secondary: number | null; + /** + * Per-stage minutes for a sleep night point. `null` for + * every non-sleep kind. Lets a sleep detail view render the night's + * stage breakdown without a second fetch; the headline `value` stays + * the night's TIME-ASLEEP total. + */ + sleepStages?: Partial> | null; + /** + * (iOS #34 extension) — per-point spread for `kind=pulse`. On an + * aggregated hourly heart-rate bucket `value` is the hour's AVERAGE bpm + * and these carry the hour's MIN / MAX. `null` for a per-sample PULSE row + * (no bucket spread) and absent for every non-pulse kind, so a client can + * render a low/high band only where the data supports it. + */ + valueMin?: number | null; + valueMax?: number | null; +} + +export function stdDev(values: number[]): number { + if (values.length < 2) return 0; + const sd = standardDeviation(values, READING_SPREAD) ?? 0; + return Math.round(sd * 100) / 100; +} + +/** The stat strip, when a reader computes it from the raw rows itself. */ +export interface SeriesStats { + mean: number; + min: number; + max: number; + stdDev: number; + count: number; +} + +/** What one reader hands back: the points, and the unit and stats it fixed. */ +export interface SeriesRead { + points: SeriesPoint[]; + unit?: string; + statsOverride?: SeriesStats | null; +} diff --git a/src/lib/measurements/series/read-blood-pressure.ts b/src/lib/measurements/series/read-blood-pressure.ts new file mode 100644 index 000000000..89c1f5917 --- /dev/null +++ b/src/lib/measurements/series/read-blood-pressure.ts @@ -0,0 +1,127 @@ +/** + * The blood-pressure series: systolic points paired with the diastolic of + * the same source within five minutes, from the ladder's one source per day. + */ +import { prisma } from "@/lib/db"; +import { VALUE_RANGES } from "@/lib/validations/measurement"; +import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; +import { buildSourceRankCase } from "@/lib/analytics/source-rank-sql"; + +import type { SeriesRead } from "./kinds"; + +export async function readBloodPressureSeries( + userId: string, + since: Date, +): Promise { + // Exclude non-physiological rows (systolic < 40 / diastolic < 20) from the + // series AND the implicit "latest" (the last point). BP is two rows paired + // client-side, so a corrupt row that slipped in before the input floor was + // enforced — a seed-era systolic-0, iOS #33 — must never surface as the + // latest reading. Floors mirror VALUE_RANGES (the input validator's min); + // this is a read-side selection guard, not a tightening of the write floor. + // + // The ladder picks ONE source per day for the reading, not one per type: + // the source is chosen from the day's systolic rows that pass the floor, + // and both halves are read from it. Two per-type picks diverge whenever a + // source holds only half of a reading on a day (a deleted diastolic, a + // sub-floor systolic, an orphaned sample), and the pairing below then + // joined one source's systolic to another source's diastolic. Picking on + // floor-passing systolic rows also keeps a corrupt row from claiming a day + // it then contributes nothing to. The day is the UTC day, as in every + // other ladder read (`canonicalMeasurementsCte`), and the pick looks at + // the whole edge day, not only the slice after `since`. + // + // `rank` is the closed-enum whitelist splice from `buildSourceRankCase`; + // every request value is a positional parameter. + const rank = buildSourceRankCase( + await loadUserSourcePriority(userId), + '"type"', + '"source"', + ); + const bpRows = await prisma.$queryRawUnsafe< + Array<{ + id: string; + type: "BLOOD_PRESSURE_SYS" | "BLOOD_PRESSURE_DIA"; + source: string; + value: number; + measured_at: Date; + }> + >( + ` + SELECT m."id", m."type"::text AS type, m."source"::text AS source, + m."value", m."measured_at" + FROM measurements m + JOIN ( + SELECT DISTINCT ON (date_trunc('day', "measured_at")) + date_trunc('day', "measured_at") AS d, + "source" AS canon + FROM measurements + WHERE "user_id" = $1 + AND "type" = 'BLOOD_PRESSURE_SYS'::"measurement_type" + AND "deleted_at" IS NULL + AND "value" >= $3 + AND "measured_at" >= date_trunc('day', $2::timestamptz AT TIME ZONE 'UTC') + ORDER BY date_trunc('day', "measured_at"), (${rank}), "source"::text + ) c + ON c.d = date_trunc('day', m."measured_at") + AND c.canon = m."source" + WHERE m."user_id" = $1 + AND m."deleted_at" IS NULL + AND m."measured_at" >= $2 + AND ( + (m."type" = 'BLOOD_PRESSURE_SYS'::"measurement_type" AND m."value" >= $3) + OR (m."type" = 'BLOOD_PRESSURE_DIA'::"measurement_type" AND m."value" >= $4) + ) + ORDER BY m."measured_at" ASC, m."id" ASC + `, + userId, + since, + VALUE_RANGES.BLOOD_PRESSURE_SYS.min, + VALUE_RANGES.BLOOD_PRESSURE_DIA.min, + ); + const sys = bpRows.filter((r) => r.type === "BLOOD_PRESSURE_SYS"); + // Diastolic rows per source: a pair is only ever made inside one source, + // which also holds across midnight, where the two days may have picked + // different sources. + const diaBySource = new Map(); + for (const r of bpRows) { + if (r.type !== "BLOOD_PRESSURE_DIA") continue; + const list = diaBySource.get(r.source); + if (list) list.push(r); + else diaBySource.set(r.source, [r]); + } + + // Pair by closest timestamp within ±5 minutes. Both reads arrive + // sorted ascending, so the nearest-diastolic index is non-decreasing + // across the systolic walk — a two-pointer merge (one pointer per + // source) finds each pair in O(sys + dia) instead of the previous full + // diastolic rescan per systolic row (O(sys × dia) — quadratic on dense + // imports). Ties keep the earlier diastolic row, matching the old + // scan's strict-`<` keep. + const PAIR_WINDOW_MS = 5 * 60_000; + const diaIdx = new Map(); + const points = sys.map((s) => { + const sysMs = s.measured_at.getTime(); + const dia = diaBySource.get(s.source) ?? []; + let idx = diaIdx.get(s.source) ?? 0; + while ( + idx + 1 < dia.length && + Math.abs(dia[idx + 1].measured_at.getTime() - sysMs) < + Math.abs(dia[idx].measured_at.getTime() - sysMs) + ) { + idx += 1; + } + diaIdx.set(s.source, idx); + const best = dia[idx]; + const paired = + best !== undefined && + Math.abs(best.measured_at.getTime() - sysMs) <= PAIR_WINDOW_MS; + return { + id: s.id, + at: s.measured_at.toISOString(), + value: s.value, + secondary: paired ? best.value : null, + }; + }); + return { points }; +} diff --git a/src/lib/measurements/series/read-bucketed.ts b/src/lib/measurements/series/read-bucketed.ts new file mode 100644 index 000000000..9dc49a0d5 --- /dev/null +++ b/src/lib/measurements/series/read-bucketed.ts @@ -0,0 +1,152 @@ +/** + * The bucketed series of a sample-dense kind: day buckets past the raw + * window, hour buckets for a stream too dense to send raw inside it, with + * the stat strip aggregated over every raw reading in SQL. + */ +import { prisma } from "@/lib/db"; +import { readDenseSeries } from "@/lib/measurements/dense-series-read"; +import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; +import { seriesRowsFrom } from "@/lib/measurements/series-canonical"; +import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; +import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose"; + +import { + DENSE_SERIES_KINDS, + DENSE_SERIES_RAW_ROW_CAP, + DENSE_SERIES_RAW_WINDOW_DAYS, + KIND_TO_TYPE, + type SeriesKind, + type SeriesPoint, + type SeriesRead, + type SeriesStats, +} from "./kinds"; + +/** Whether this request takes the bucketed read. */ +export async function takesBucketedRead( + userId: string, + kind: SeriesKind, + days: number, + since: Date, +): Promise { + return ( + (DENSE_SERIES_KINDS.has(kind) && days > DENSE_SERIES_RAW_WINDOW_DAYS) || + (DENSE_SERIES_KINDS.has(kind) && + (await prisma.measurement.count({ + where: { + userId, + type: KIND_TO_TYPE[kind], + measuredAt: { gte: since }, + deletedAt: null, + }, + })) > DENSE_SERIES_RAW_ROW_CAP) + ); +} + +export async function readBucketedSeries( + userId: string, + kind: SeriesKind, + days: number, + since: Date, +): Promise { + let points: SeriesPoint[] = []; + let unit: string | undefined; + let statsOverride: SeriesStats | null = null; + // Day buckets past the raw window; hour buckets for a dense-kind + // stream too dense to send raw inside it. + const grain = days > DENSE_SERIES_RAW_WINDOW_DAYS ? "day" : "hour"; + // Long-window read of a sample-dense kind (CGM glucose, + // per-sample / hourly pulse). Day-bucket in SQL instead of walking + // every raw row into JS: one aggregate pass in Postgres returns at + // most `days` rows regardless of sample density. Bound parameters + // mirror the rollup tier's `date_trunc(... ) GROUP BY` aggregate + // (measurement-rollups.ts), and the rows are the ladder-winning source per + // day, the same rule the rollup tier and sleep apply. + const [userTz, priorityJson] = await Promise.all([ + resolveUserTimezone(userId), + loadUserSourcePriority(userId), + ]); + const type = KIND_TO_TYPE[kind]; + const seriesRows = seriesRowsFrom(priorityJson, type, days); + // A day of a type with an activity-dependent sampling rate (pulse) is the + // weighted mean of its local hours' means, so a workout hour with hundreds + // of times the readings does not outweigh the rest of the day (see + // `day-statistic.ts`). The hour grain is already one mean per hour. + const { bucketRows, rawAgg } = await readDenseSeries({ + userId: userId, + type, + grain, + userTz, + since, + seriesRows, + }); + const round2 = (v: number) => Math.round(v * 100) / 100; + const dayId = (d: Date) => + grain === "day" + ? `day:${userDayKey(d, userTz)}` + : `hour:${d.toISOString()}`; + if (kind === "glucose") { + // Same unit engine as the raw glucose branch: canonical mg/dL in + // the DB, converted ONCE at serialization to the user's preference. + const profile = await prisma.user.findUnique({ + where: { id: userId }, + select: { glucoseUnit: true }, + }); + const glucoseUnit = resolveGlucoseUnit(profile?.glucoseUnit ?? null); + unit = glucoseUnit; + points = bucketRows.map((r) => ({ + id: dayId(r.bucket_start), + at: r.bucket_start.toISOString(), + value: convertGlucose(r.mean, glucoseUnit), + secondary: null, + })); + if ( + rawAgg !== undefined && + rawAgg.n > 0 && + rawAgg.mean !== null && + rawAgg.min !== null && + rawAgg.max !== null + ) { + // Mirrors the raw branch's v1.16.16 parity: round the mg/dL + // aggregate to 2 decimals (summarize()'s convention), then + // convert each figure once. + statsOverride = { + mean: convertGlucose(round2(rawAgg.mean), glucoseUnit), + min: convertGlucose(rawAgg.min, glucoseUnit), + max: convertGlucose(rawAgg.max, glucoseUnit), + stdDev: convertGlucose(round2(rawAgg.sd ?? 0), glucoseUnit), + count: rawAgg.n, + }; + } + } else { + // `value` is the bucket's average. For pulse, valueMin/valueMax carry + // the bucket's low/high band (folding each hourly import bucket's own + // spread via the COALESCE above), same band semantics as the per-hour + // shape. The other dense kinds keep the bare point shape their raw rows + // have, so a client reads a bucket the way it reads a reading. + points = bucketRows.map((r) => ({ + id: dayId(r.bucket_start), + at: r.bucket_start.toISOString(), + value: round2(r.mean), + secondary: null, + ...(kind === "pulse" + ? { valueMin: r.min_value, valueMax: r.max_value } + : {}), + })); + if ( + rawAgg !== undefined && + rawAgg.n > 0 && + rawAgg.mean !== null && + rawAgg.min !== null && + rawAgg.max !== null + ) { + statsOverride = { + mean: round2(rawAgg.mean), + min: rawAgg.min, + max: rawAgg.max, + stdDev: round2(rawAgg.sd ?? 0), + count: rawAgg.n, + }; + } + } + return { points, unit, statsOverride }; +} diff --git a/src/lib/measurements/series/read-raw.ts b/src/lib/measurements/series/read-raw.ts new file mode 100644 index 000000000..39a1db0b6 --- /dev/null +++ b/src/lib/measurements/series/read-raw.ts @@ -0,0 +1,165 @@ +/** + * The raw series: the ladder-winning source per day, row by row, with the + * glucose unit and the hourly-mean stat strip applied where they belong. + */ +import { prisma } from "@/lib/db"; +import { summarize, type DataPoint } from "@/lib/analytics/trends"; +import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; +import { usesHourlyMeanDay } from "@/lib/measurements/day-statistic"; +import { readingsMean } from "@/lib/measurements/day-mean"; +import { seriesRowsFrom } from "@/lib/measurements/series-canonical"; +import { resolveUserTimezone } from "@/lib/tz/resolver"; +import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose"; + +import { + KIND_TO_TYPE, + stdDev, + type SeriesKind, + type SeriesPoint, + type SeriesRead, + type SeriesStats, +} from "./kinds"; + +export async function readRawSeries( + userId: string, + kind: SeriesKind, + days: number, + since: Date, +): Promise { + let points: SeriesPoint[] = []; + let unit: string | undefined; + let statsOverride: SeriesStats | null = null; + const type = KIND_TO_TYPE[kind]; + // (iOS #34 extension) — pull the per-bucket spread for the + // heart-rate kind so the chart can render a low/high band around the + // hourly average. Only PULSE rows ever carry a non-null spread (the + // hourly HR bucket); every other kind selects the bare value shape. + const includeSpread = kind === "pulse"; + // The ladder-winning source per day (see `seriesRowsFrom`), so a second + // provider neither blends into nor duplicates the series. Bounded as before: + // every dense kind reaches this read only inside 90 days and under 10 000 + // rows (counted first), and the sparse kinds are a few readings a day. + const sourcePriority = await loadUserSourcePriority(userId); + const rawRows = await prisma.$queryRawUnsafe< + Array<{ + id: string; + value: number; + measured_at: Date; + value_min: number | null; + value_max: number | null; + }> + >( + ` + SELECT m."id", m."value", m."measured_at", m."value_min", m."value_max" + FROM ${seriesRowsFrom(sourcePriority, type, days)} + WHERE m."measured_at" >= $2 + ORDER BY m."measured_at" ASC, m."id" ASC + `, + userId, + since, + ); + const rows = rawRows.map((r) => ({ + id: r.id, + value: r.value, + measuredAt: r.measured_at, + ...(includeSpread ? { valueMin: r.value_min, valueMax: r.value_max } : {}), + })); + if (kind === "glucose") { + // Glucose is stored canonical mg/dL; convert each point to + // the user's display unit AT SERIALIZATION so the wire DTO is unit- + // coherent with the CSV + FHIR exports (one number, one engine). The + // top-level `unit` token follows. mg/dL-preference users are unchanged + // (convertGlucose rounds the integer); mmol/L users see 1-decimal + // values (100 → 5.5) and `unit: "mmol/L"`. + const profile = await prisma.user.findUnique({ + where: { id: userId }, + select: { glucoseUnit: true }, + }); + const glucoseUnit = resolveGlucoseUnit(profile?.glucoseUnit ?? null); + unit = glucoseUnit; + points = rows.map((r) => ({ + id: r.id, + at: r.measuredAt.toISOString(), + value: convertGlucose(r.value, glucoseUnit), + secondary: null, + })); + // Stat parity. The stat strip's mean/min/max/stdDev must + // match the detail-page + FHIR convention: aggregate over RAW mg/dL, + // then convert each resulting figure ONCE. Deriving the stats from the + // already-converted+rounded points double-rounds the mean and stdDev + // for mmol/L users, drifting the last decimal off the detail page. + // Single-reading sets stay identical (100 → 5.5). Hand back stats here + // so the shared block below leaves a populated `statsOverride` alone. + const rawDataPoints: DataPoint[] = rows.map((r) => ({ + date: r.measuredAt, + value: r.value, + })); + if (rawDataPoints.length > 0) { + const rawSummary = summarize(rawDataPoints); + const rawValues = rawDataPoints.map((p) => p.value); + statsOverride = { + mean: + rawSummary.mean === null + ? 0 + : convertGlucose(rawSummary.mean, glucoseUnit), + min: + rawSummary.min === null + ? 0 + : convertGlucose(rawSummary.min, glucoseUnit), + max: + rawSummary.max === null + ? 0 + : convertGlucose(rawSummary.max, glucoseUnit), + // Glucose mg/dL↔mmol/L is purely multiplicative (no offset), so a + // spread converts with the same factor as a level. + stdDev: convertGlucose(stdDev(rawValues), glucoseUnit), + count: rawSummary.count, + }; + } + } else if (includeSpread) { + // (iOS #34 extension) — pulse points carry the per-bucket + // spread. A per-sample PULSE row has null min/max; an hourly HR bucket + // carries the hour's range around the average. + points = rows.map((r) => { + const row = r as typeof r & { + valueMin: number | null; + valueMax: number | null; + }; + return { + id: row.id, + at: row.measuredAt.toISOString(), + value: row.value, + secondary: null, + valueMin: row.valueMin ?? null, + valueMax: row.valueMax ?? null, + }; + }); + // An hourly-mean type's strip mean is the mean of its local days' + // values, each day the mean of its local hours' means, so a dense + // workout hour does not outweigh the rest of the window. Count, min, + // max and the spread stay over every reading. + if (usesHourlyMeanDay(type) && rows.length > 0) { + const userTz = await resolveUserTimezone(userId); + const values = rows.map((r) => r.value); + const rawSummary = summarize( + rows.map((r) => ({ date: r.measuredAt, value: r.value })), + ); + const mean = readingsMean(type, rows, userTz); + statsOverride = { + mean: mean === null ? 0 : Math.round(mean * 100) / 100, + min: rawSummary.min ?? 0, + max: rawSummary.max ?? 0, + stdDev: stdDev(values), + count: rawSummary.count, + }; + } + } else { + points = rows.map((r) => ({ + id: r.id, + at: r.measuredAt.toISOString(), + value: r.value, + secondary: null, + })); + } + return { points, unit, statsOverride }; +} diff --git a/src/lib/measurements/series/read-series.ts b/src/lib/measurements/series/read-series.ts new file mode 100644 index 000000000..3061619ea --- /dev/null +++ b/src/lib/measurements/series/read-series.ts @@ -0,0 +1,148 @@ +/** + * `GET /api/measurements/series` after its query has been validated: pick the + * reader the kind and window call for, then add the judgements a client would + * otherwise make itself — the change direction, the change over the window, + * the reference lines — and the stat strip. + */ +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import { summarize, type DataPoint } from "@/lib/analytics/trends"; +import { resolveUserTimezone } from "@/lib/tz/resolver"; +import { resolveGlucoseUnit } from "@/lib/glucose"; +import { resolveTrendVerdict } from "@/lib/targets/trend-verdict"; +import { + computeSeriesChange, + seriesDayValues, + seriesReferenceValue, +} from "@/lib/measurements/series-change"; +import { resolveSeriesReferenceLines } from "@/lib/measurements/series-reference-lines"; +import { getServerTranslator } from "@/lib/i18n/server-translator"; +import { resolveJobLocale } from "@/lib/i18n/job-locale"; + +import { + KIND_TO_TYPE, + SERIES_UNIT, + stdDev, + type SeriesKind, + type SeriesRead, +} from "./kinds"; +import { readBloodPressureSeries } from "./read-blood-pressure"; +import { readBucketedSeries, takesBucketedRead } from "./read-bucketed"; +import { readRawSeries } from "./read-raw"; +import { readSleepSeries } from "./read-sleep"; + +/** The reader this kind and window call for, in the route's old order. */ +async function readPoints( + userId: string, + kind: SeriesKind, + days: number, + since: Date, +): Promise { + if (kind === "sleep") return readSleepSeries(userId, since, days); + if (kind === "bloodPressure") return readBloodPressureSeries(userId, since); + if (await takesBucketedRead(userId, kind, days, since)) { + return readBucketedSeries(userId, kind, days, since); + } + return readRawSeries(userId, kind, days, since); +} + +export async function readSeries(args: { + /** The record read (the owner under a delegation). */ + userId: string; + /** Who is reading: the labels are in their language. */ + actorId: string; + kind: SeriesKind; + days: number; +}) { + const { userId, actorId, kind, days } = args; + const since = new Date(Date.now() - days * 86_400_000); + + const read = await readPoints(userId, kind, days, since); + // An explicit unit token so the client never infers the unit from the + // kind. Sleep is served in hours and glucose in the user's unit; every + // other kind carries its canonical stored unit. + const unit = read.unit ?? SERIES_UNIT[kind]; + const points = read.points; + const statsOverride = read.statsOverride ?? null; + + const dataPoints: DataPoint[] = points.map((p) => ({ + date: new Date(p.at), + value: p.value, + })); + const summary = dataPoints.length > 0 ? summarize(dataPoints) : null; + const values = dataPoints.map((p) => p.value); + + // The resolved judgements. The profile and targets are the + // RECORD's (the chart is theirs); the labels are in the language of the + // account reading it. + const [record, reader, seriesTz] = await Promise.all([ + prisma.user.findUnique({ + where: { id: userId }, + select: { + dateOfBirth: true, + gender: true, + heightCm: true, + thresholdsJson: true, + glucoseUnit: true, + locale: true, + }, + }), + actorId === userId + ? null + : prisma.user.findUnique({ + where: { id: actorId }, + select: { locale: true }, + }), + resolveUserTimezone(userId), + ]); + const rangeProfile = { + dateOfBirth: record?.dateOfBirth ?? null, + gender: record?.gender ?? null, + heightCm: record?.heightCm ?? null, + }; + const verdictType = KIND_TO_TYPE[kind]; + const dayValues = seriesDayValues(verdictType, points, seriesTz); + const reference = seriesReferenceValue(dayValues); + const direction = resolveTrendVerdict(verdictType, { + profile: rangeProfile, + thresholdsJson: record?.thresholdsJson ?? null, + // The verdict reads stored units: sleep is served in hours here and + // stored in minutes. Glucose has no band of its own on this chart. + referenceValue: + reference !== null && kind === "sleep" ? reference * 60 : reference, + }); + const translator = getServerTranslator( + await resolveJobLocale((reader ?? record)?.locale ?? null), + ); + const referenceLines = resolveSeriesReferenceLines(kind, { + profile: rangeProfile, + thresholdsJson: record?.thresholdsJson ?? null, + glucoseUnit: resolveGlucoseUnit(record?.glucoseUnit ?? null), + t: (key) => translator.t(key), + }); + + annotate({ + action: { name: "measurements.series" }, + meta: { kind, days, count: points.length }, + }); + + return { + kind, + unit, + points, + direction, + change: computeSeriesChange(dayValues, direction), + referenceLines, + stats: + statsOverride ?? + (summary + ? { + mean: summary.mean, + min: summary.min, + max: summary.max, + stdDev: stdDev(values), + count: summary.count, + } + : { mean: 0, min: 0, max: 0, stdDev: 0, count: 0 }), + }; +} diff --git a/src/lib/measurements/series/read-sleep.ts b/src/lib/measurements/series/read-sleep.ts new file mode 100644 index 000000000..b6f95a008 --- /dev/null +++ b/src/lib/measurements/series/read-sleep.ts @@ -0,0 +1,57 @@ +/** + * The sleep series: one point per night carrying its time asleep in hours. + */ +import { readSleepNights } from "@/lib/measurements/list/sleep-nights"; +import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; +import { resolveUserTimezone } from "@/lib/tz/resolver"; +import type { SleepStage } from "@/generated/prisma/client"; + +import type { SeriesRead } from "./kinds"; + +export async function readSleepSeries( + userId: string, + since: Date, + days: number, +): Promise { + // SLEEP_DURATION is stored one row per STAGE per night (minutes). + // Collapse the stage rows into ONE point per night carrying the + // night's TIME ASLEEP (CORE + DEEP + REM, excluding IN_BED + AWAKE), + // converted to HOURS. The single-stage rows the legacy path returned + // made the chart show one fragment per stage instead of a nightly + // trend. The per-night reconstruction clusters stages into sessions + // (so a midnight-spanning night stays one point) and collapses a + // dual-source night to one canonical source via the user's `sleep` + // priority ladder. + // + // Sleep is the one kind read row-per-stage rather than from the rollup + // tier, so the nights are named in Postgres and reconstructed a chunk at + // a time (`readSleepNights`): "All" reaches as far back as the history + // does, and the stage rows in the heap stay a chunk's worth. + const [tz, priorityJson] = await Promise.all([ + resolveUserTimezone(userId), + loadUserSourcePriority(userId), + ]); + const nights = await readSleepNights(userId, tz, priorityJson, { + from: since, + // A window of `days` days holds at most one night more than that. + maxNights: days + 1, + }); + const points = nights + .filter((n) => n.asleepMinutes > 0) + .map((n) => { + const stageHours = Object.fromEntries( + Object.entries(n.stages).map(([s, m]) => [ + s, + Math.round((m / 60) * 100) / 100, + ]), + ) as Partial>; + return { + id: `sleep:${n.night}`, + at: n.measuredAt.toISOString(), + value: Math.round((n.asleepMinutes / 60) * 100) / 100, + secondary: null, + sleepStages: Object.keys(stageHours).length > 0 ? stageHours : null, + }; + }); + return { points, unit: "h" }; +} diff --git a/src/lib/medications/__tests__/auto-mark-taken.test.ts b/src/lib/medications/__tests__/auto-mark-taken.test.ts index 0f04c842b..7c072ee97 100644 --- a/src/lib/medications/__tests__/auto-mark-taken.test.ts +++ b/src/lib/medications/__tests__/auto-mark-taken.test.ts @@ -83,7 +83,7 @@ describe("sendsMedicationReminders", () => { } for (const dto of [ "lib/medications/list-read.ts", - "app/api/medications/[id]/route.ts", + "lib/medications/read-medication.ts", ]) { expect(src(dto), dto).toMatch( /remindersActive:\s*sendsMedicationReminders\(/, diff --git a/src/lib/medications/delete-medication.ts b/src/lib/medications/delete-medication.ts new file mode 100644 index 000000000..59d0f5a60 --- /dev/null +++ b/src/lib/medications/delete-medication.ts @@ -0,0 +1,77 @@ +/** + * Delete one medication: `DELETE /api/medications/{id}`. + * + * The route authenticates (the caller's own account) and runs the ownership + * guard; the token revocation, the delete, its audit row and its tails run + * here, in the order they always ran. + */ +import { prisma } from "@/lib/db"; +import { + enqueueSupplementNutrientRecompute, + medicationHasComposition, +} from "@/lib/medications/ingredients/recompute-job"; +import { annotate } from "@/lib/logging/context"; +import { auditLog } from "@/lib/auth/audit"; +import { invalidateUserMedications } from "@/lib/cache/invalidate"; +import { deleteMedicationCategory } from "@/lib/medication-category"; + +/** + * The name the audit row carries, read after the ownership guard passed. + * Null for the route's 404. + */ +export async function findMedicationForDelete( + id: string, +): Promise<{ name: string } | null> { + return prisma.medication.findUnique({ + where: { id }, + select: { name: true }, + }); +} + +export async function deleteMedication(input: { + userId: string; + id: string; + existing: { name: string }; + ipAddress: string | null; +}): Promise { + const { userId, id, existing } = input; + + // Revoke API tokens scoped to this medication + const medicationScope = `medication:${id}:ingest`; + await prisma.apiToken.updateMany({ + where: { + userId, + revoked: false, + permissions: { has: medicationScope }, + }, + data: { revoked: true }, + }); + + // Whether its doses counted toward the micronutrients, read + // before the delete cascades the composition away. + const hadComposition = await medicationHasComposition(userId, id); + + await deleteMedicationCategory(id); + await prisma.medication.delete({ where: { id } }); + + await auditLog("medication.delete", { + userId, + ipAddress: input.ipAddress, + details: { medicationId: id, name: existing.name }, + }); + + annotate({ + action: { + name: "medication.delete", + entity_type: "medication", + entity_id: id, + }, + }); + + // Bust per-user medications + compliance + achievement + // caches so the next read reflects the deletion. + invalidateUserMedications(userId, { evict: true }); + + // Its doses are gone with it, so the days they counted on are recomputed. + if (hadComposition) await enqueueSupplementNutrientRecompute(userId); +} diff --git a/src/lib/medications/intake-bulk/batch-state.ts b/src/lib/medications/intake-bulk/batch-state.ts new file mode 100644 index 000000000..d9d1f2397 --- /dev/null +++ b/src/lib/medications/intake-bulk/batch-state.ts @@ -0,0 +1,102 @@ +/** + * What a bulk intake batch accumulates while its entries are walked: the + * per-entry results and counters the response carries, and the distinct + * days, slots and medications the tails after the loop act on. + */ +import type { UnstableExternalIdShape } from "@/lib/validations/external-id"; +import type { InjectionSiteValue } from "@/lib/validations/medication"; + +/** + * One entry of `POST /api/medications/intake/bulk`, as the route's schema + * parses it. + */ +export interface BulkIntakeEntry { + medicationId: string; + scheduledFor?: Date; + takenAt?: Date; + skipped: boolean; + idempotencyKey?: string; + injectionSite?: InjectionSiteValue; + forceSlotInstant?: Date; + doseTaken?: string; + source?: "APPLE_HEALTH"; + externalId?: string; +} + +/** + * `updated` joins the per-entry status vocabulary: a write that snaps onto + * an existing scheduled-slot row (e.g. the pending REMINDER row) updates it + * in place rather than inserting. The iOS sync engine treats `updated` the + * same as `inserted` for cursor advancement — both mean "the server accepted + * this entry and produced a row id". + */ +type EntryStatus = "inserted" | "updated" | "duplicate" | "skipped"; +export interface EntryResult { + index: number; + status: EntryStatus; + reason?: string; + id?: string; +} + +export interface BatchState { + results: EntryResult[]; + inserted: number; + updated: number; + duplicates: number; + /** + * External dose records kept as their own unscheduled dose because their + * band slot already carried a different one. + */ + extraDosesUnscheduled: number; + skipped: Array<{ index: number; reason: string }>; + unstableShapes: UnstableExternalIdShape[]; + /** + * Distinct `(medicationId, dayKey)` pairs touched by the batch, so one + * dose-day refresh fires per pair after all inserts complete. A per-row + * refresh would balloon a 500-entry batch into 500 sequential refreshes. + */ + touchedDays: Map; + /** + * Distinct `(medicationId, scheduledFor)` slots whose state actually + * changed in this batch (#22: a row inserted / updated), so the silent + * cross-device sync fires once per affected slot — not once per row, and + * not for pending echoes / duplicates that changed nothing. + */ + syncSlots: Map; + /** + * A landed non-skipped dose quiets an active snooze, exactly like the + * single-intake routes (they null `snoozedUntil` on every recorded take). + * Collected per medication here, cleared once after the loop. + */ + snoozeClearIds: Set; + /** + * Live rows already carrying one of the batch's external dose-event UUIDs, + * and the rows this batch landed under one, so an intra-batch repeat of a + * UUID dedups too. + */ + rowIdByExternalId: Map; +} + +export function createBatchState( + rowIdByExternalId: Map, +): BatchState { + return { + results: [], + inserted: 0, + updated: 0, + duplicates: 0, + extraDosesUnscheduled: 0, + skipped: [], + unstableShapes: [], + touchedDays: new Map(), + syncSlots: new Map(), + snoozeClearIds: new Set(), + rowIdByExternalId, + }; +} + +/** Record an entry the batch skipped, with its reason. */ +export function skipEntry(state: BatchState, index: number, reason: string) { + state.skipped.push({ index, reason }); + state.results.push({ index, status: "skipped", reason }); +} diff --git a/src/lib/medications/intake-bulk/entry-attribution.ts b/src/lib/medications/intake-bulk/entry-attribution.ts new file mode 100644 index 000000000..aa509ba96 --- /dev/null +++ b/src/lib/medications/intake-bulk/entry-attribution.ts @@ -0,0 +1,178 @@ +/** + * Which slot one bulk intake entry binds to. + * + * Taken writes attribute through the window-band engine (the SAME minter the + * read ledger + the compliance % consume), so a bulk-synced take binds to a + * slot identically to the single-route write. The optional + * `forceSlotInstant` pins an off-window take onto a named real slot. Pending + * echoes and skips carry no take to attribute by, so they keep the canonical + * anchor snap that binds them to the projector/worker-minted slot row. + */ +import type { SessionUser } from "@/lib/auth/session-user"; +import { + findPinConflict, + resolveForcedSlotForWrite, + resolveSlotForWriteByBand, + resolveSlotInstantForWrite, + slotHeldByAnotherDose, +} from "@/lib/medications/scheduling/slot-upsert"; +import type { BulkIntakeEntry } from "./batch-state"; + +export interface EntryAttribution { + canonicalSlot: Date | null; + /** + * Binding provenance: USER_PIN on the forced path, AUTO on a band + * decision. Pending echoes / skips carry no decision (undefined → the + * upsert leaves an existing USER_PIN untouched). + */ + attributionSource: "AUTO" | "USER_PIN" | undefined; + /** + * Set when the band slot is held by a different dose and this take is + * recorded unscheduled; it must not converge onto any slot row. + */ + heldByAnotherDose: boolean; +} + +interface EntryContext { + user: Pick; + entry: BulkIntakeEntry; + incomingScheduledFor: Date; +} + +async function attributePin( + ctx: EntryContext, + forceSlotInstant: Date, +): Promise { + const { user, entry } = ctx; + const canonicalSlot = await resolveForcedSlotForWrite({ + userId: user.id, + medicationId: entry.medicationId, + userTz: user.timezone, + slotInstant: forceSlotInstant, + }); + if (canonicalSlot === null) { + // Mirrors the single route's 422 (`medications.intake.force_slot + // .invalid`) as a per-entry skip so one bad pin never fails the + // batch. + return { reason: "force_slot_invalid" }; + } + // Refuse to pin onto a slot another recorded action + // already serves: the explicit-write last-write-wins rule would + // silently overwrite that dose record. Per-entry skip, mirroring + // the single route's 422 `medications.intake.force_slot.occupied`. + if ( + await findPinConflict({ + userId: user.id, + medicationId: entry.medicationId, + canonicalSlot, + incomingTakenAt: entry.takenAt ?? null, + }) + ) { + return { reason: "force_slot_occupied" }; + } + return { + canonicalSlot, + attributionSource: "USER_PIN", + heldByAnotherDose: false, + }; +} + +async function attributeTake(ctx: EntryContext): Promise { + const { user, entry } = ctx; + const attribution = await resolveSlotForWriteByBand({ + userId: user.id, + medicationId: entry.medicationId, + userTz: user.timezone, + takenAt: entry.takenAt as Date, + }); + let canonicalSlot = attribution.slotInstant; + let heldByAnotherDose = false; + // An external dose record (a HealthKit dose event) whose band slot + // already carries a different recorded dose is an extra dose, not a + // correction of that one. Converging would overwrite the slot's + // take last-write-wins and the extra dose would exist nowhere; it is + // recorded as its own unscheduled dose instead. + if ( + canonicalSlot !== null && + entry.externalId !== undefined && + (await slotHeldByAnotherDose({ + userId: user.id, + medicationId: entry.medicationId, + canonicalSlot, + takenAt: entry.takenAt as Date, + externalId: entry.externalId, + scheduledFor: entry.scheduledFor ?? null, + userTz: user.timezone, + })) + ) { + canonicalSlot = null; + heldByAnotherDose = true; + } + return { canonicalSlot, attributionSource: "AUTO", heldByAnotherDose }; +} + +/** + * The slot of one entry, or the per-entry skip reason of a bad pin. The + * write flags say what the entry is: an offline-sync replay echoes a PENDING + * projection (no `takenAt`, `skipped:false`) for a slot the user may already + * have actioned; an explicit `takenAt` or `skipped:true` is a real user + * action and applies last-write-wins. + */ +export async function attributeEntry( + ctx: EntryContext, + flags: { isExplicitTaken: boolean; isExplicitSkip: boolean }, +): Promise { + const { user, entry, incomingScheduledFor } = ctx; + if (flags.isExplicitTaken && entry.forceSlotInstant !== undefined) { + return attributePin(ctx, entry.forceSlotInstant); + } + if (flags.isExplicitTaken) { + return attributeTake(ctx); + } + if (flags.isExplicitSkip && entry.scheduledFor === undefined) { + // A slot-less deliberate skip resolves by band + // membership on the skip moment, exactly like a take does. The + // defaulted-now snap below only reaches the tight reminder-grace + // window, so a skip tapped 20 minutes past the dose time inserted + // an orphan ad-hoc skip row while the slot's pending REMINDER row + // stayed open and later auto-missed — the user's deliberate skip + // recorded as a miss. + const attribution = await resolveSlotForWriteByBand({ + userId: user.id, + medicationId: entry.medicationId, + userTz: user.timezone, + takenAt: incomingScheduledFor, + }); + return { + canonicalSlot: attribution.slotInstant, + attributionSource: undefined, + heldByAnotherDose: false, + }; + } + // Source-agnostic anchor snap for pending echoes + skips. + // iOS posts the reminder actions here (source API); without this + // they inserted a SECOND row for a slot already carrying the + // projector/worker's pending REMINDER row (the unique key includes + // `source`, and the iOS-vs-server `scheduledFor` drifts by a + // minute). Snap onto the canonical slot instant and update the + // existing row in place. + const canonicalSlot = await resolveSlotInstantForWrite({ + userId: user.id, + medicationId: entry.medicationId, + userTz: user.timezone, + incoming: incomingScheduledFor, + // Only a client-supplied `scheduledFor` names a real slot. A + // defaulted-now write must not snap across the wide ±halfGap + // window onto a far slot (phantom morning dose). + instantIsExplicit: entry.scheduledFor !== undefined, + // Pending echoes / skips never record a take; a pending sync + // echo legitimately maps to a future slot, so the taken + // future-slot guard stays off here. + isTakenWrite: false, + }); + return { + canonicalSlot, + attributionSource: undefined, + heldByAnotherDose: false, + }; +} diff --git a/src/lib/medications/intake-bulk/entry-write.ts b/src/lib/medications/intake-bulk/entry-write.ts new file mode 100644 index 000000000..2a6c6b045 --- /dev/null +++ b/src/lib/medications/intake-bulk/entry-write.ts @@ -0,0 +1,251 @@ +/** + * The row one bulk intake entry lands on: the canonical slot write, the + * idempotency replay, the convergence probe onto a row already on the named + * instant, or the standalone insert, with the inventory hook each one owes. + */ +import { prisma } from "@/lib/db"; +import type { SessionUser } from "@/lib/auth/session-user"; +import type { InjectionSiteKey } from "@/lib/medications/injection-sites"; +import { + applyCanonicalSlotWrite, + mayConvergeOntoSuppliedSlot, +} from "@/lib/medications/scheduling/slot-upsert"; +import { + consumeForIntake, + restoreForIntake, +} from "@/lib/medications/inventory/consumption"; +import type { BatchState, BulkIntakeEntry } from "./batch-state"; +import type { EntryAttribution } from "./entry-attribution"; + +export interface EntryWrite { + user: Pick; + entry: BulkIntakeEntry; + index: number; + incomingScheduledFor: Date; + isExplicitTaken: boolean; + isExplicitSkip: boolean; + attribution: EntryAttribution; + resolvedInjectionSite: InjectionSiteKey | null; +} + +/** + * Converge onto the one slot row through the shared upsert: H1 + * deterministic selection, C2 no-downgrade guard, and a C1 race-safe create + * that re-finds + updates on a P2002 collision rather than misclassifying it + * as a duplicate and dropping the dose. + */ +async function writeOntoSlot( + write: EntryWrite, + state: BatchState, + canonicalSlot: Date, +): Promise { + const { user, entry, index: i, isExplicitTaken, isExplicitSkip } = write; + const applied = await applyCanonicalSlotWrite({ + client: prisma, + userId: user.id, + medicationId: entry.medicationId, + canonicalSlot, + takenAt: entry.takenAt ?? null, + skipped: entry.skipped, + isExplicitTaken, + isExplicitSkip, + idempotencyKey: entry.idempotencyKey ?? null, + // An Apple dose-event import stamps its provenance on a + // fresh row; every other caller keeps today's "API". + createSource: entry.source ?? "API", + injectionSite: write.resolvedInjectionSite, + attributionSource: write.attribution.attributionSource, + // Dose override only documents a consumed dose. + doseTaken: (!entry.skipped && entry.doseTaken) || null, + // External dose-event UUID rides onto the landed row. + externalId: entry.externalId ?? null, + }); + if (applied.noDowngradeNoOp) { + // Pending echo onto an already-actioned slot. Report it as a + // duplicate so the iOS cursor advances WITHOUT downgrading the + // recorded dose. A pending echo is never a taken write, so it + // must never reach the inventory consume below. + state.duplicates += 1; + state.results.push({ index: i, status: "duplicate", id: applied.row.id }); + } else if (applied.outcome === "updated") { + state.updated += 1; + state.results.push({ index: i, status: "updated", id: applied.row.id }); + } else { + state.inserted += 1; + state.results.push({ index: i, status: "inserted", id: applied.row.id }); + } + // Inventory: only the genuine pending→taken + // transition consumes (the upsert's `consumedTransition` flag — + // the ONLY transition that may move stock). The stamp keeps a + // replayed batch exactly-once for stamped rows; the + // transition gate additionally protects pre-stamp rows: a full + // journal replay re-posting historical taken doses must not + // drain today's stock through their NULL stamps. An explicit + // skip refunds a previously-taken row's stamp. Pending echoes + // carry neither flag and never touch stock. + if (applied.consumedTransition && isExplicitTaken) { + await consumeForIntake({ + client: prisma, + userId: user.id, + medicationId: entry.medicationId, + eventId: applied.row.id, + intakeAt: entry.takenAt as Date, + }); + } else if (!applied.noDowngradeNoOp && isExplicitSkip) { + await restoreForIntake({ + client: prisma, + userId: user.id, + eventId: applied.row.id, + }); + } +} + +/** + * The idempotencyKey replay pre-check: a re-submission returns the existing + * row as "duplicate" (previously reached via the P2002 catch; the explicit + * probe keeps that contract now that a replay could otherwise converge as + * "updated"). Scoped to the caller. The key is client-supplied, so an + * unscoped read would resolve another user's row and hand its id back here + * (migration 0273 made the unique per user). + */ +async function replayedRowId(write: EntryWrite): Promise { + const { user, entry } = write; + if (!entry.idempotencyKey) return null; + const replay = await prisma.medicationIntakeEvent.findUnique({ + where: { + userId_idempotencyKey: { + userId: user.id, + idempotencyKey: entry.idempotencyKey, + }, + }, + select: { id: true }, + }); + return replay?.id ?? null; +} + +/** + * Genuinely standalone. Anchor the row on the intake instant — the + * documented ad-hoc contract (`scheduledFor = takenAt`) — so an unresolvable + * client anchor (a future slot instant the taken guard rejected) can never + * park a live row exactly on a slot the worker later mints a pending + * REMINDER row for. A pending echo (no takenAt) keeps the incoming instant. + * The idempotencyKey, when supplied, has a UNIQUE index; a racing + * re-submission returns the existing row via P2002 → status: "duplicate". + */ +async function insertStandalone( + write: EntryWrite, + state: BatchState, +): Promise { + const { user, entry, index: i, resolvedInjectionSite } = write; + const effectiveScheduledFor = entry.takenAt ?? write.incomingScheduledFor; + const row = await prisma.medicationIntakeEvent.create({ + data: { + userId: user.id, + medicationId: entry.medicationId, + scheduledFor: effectiveScheduledFor, + takenAt: entry.takenAt ?? null, + skipped: entry.skipped, + // Apple provenance on a standalone (ad-hoc) row; + // every other caller keeps today's "API". + source: entry.source ?? "API", + idempotencyKey: entry.idempotencyKey ?? null, + // External dose-event UUID for the idempotent + // re-sync dedup. + ...(entry.externalId !== undefined && { + externalId: entry.externalId, + }), + // Site only on a resolved taken-injection entry. + ...(resolvedInjectionSite !== null && { + injectionSite: resolvedInjectionSite, + }), + // Dose override only on a taken entry carrying one. + ...(!entry.skipped && entry.doseTaken && { doseTaken: entry.doseTaken }), + }, + }); + state.inserted += 1; + state.results.push({ index: i, status: "inserted", id: row.id }); + // A fresh standalone taken row consumes inventory + // units (a fresh skip / pending row has nothing to consume or + // refund). + if (write.isExplicitTaken) { + await consumeForIntake({ + client: prisma, + userId: user.id, + medicationId: entry.medicationId, + eventId: row.id, + intakeAt: entry.takenAt as Date, + }); + } + return effectiveScheduledFor; +} + +/** + * Write the entry. The answer is the `scheduledFor` instant the row actually + * lands on, so the dose-day refresh keys the same day the stored row anchors + * to, or null when an idempotency replay answered the entry and nothing + * after the write applies to it. + */ +export async function writeEntry( + write: EntryWrite, + state: BatchState, +): Promise { + const { user, entry, index: i, incomingScheduledFor } = write; + const { canonicalSlot, heldByAnotherDose } = write.attribution; + if (canonicalSlot) { + // Scheduled dose — converge onto the one canonical slot row. + await writeOntoSlot(write, state, canonicalSlot); + return canonicalSlot; + } + // Resolver-null: PRN / off-slot / future-slot-guarded. Before the + // standalone insert, two guards keep the slot from forking into a + // second live row: + // + // 1. idempotencyKey replay pre-check (see `replayedRowId`); + // 2. source-agnostic convergence probe — when ANY live row already + // sits on the incoming instant (e.g. the pending REMINDER row + // the worker pre-minted on a slot the taken-write future guard + // refused to snap to), converge onto THAT row through the shared + // slot upsert instead of inserting a sibling that differs only + // by `source`. The partial unique index carries `source`, so it + // cannot catch this duplicate; the probe must. + const replayId = await replayedRowId(write); + if (replayId !== null) { + state.duplicates += 1; + state.results.push({ index: i, status: "duplicate", id: replayId }); + return null; + } + // Only an explicit client `scheduledFor` names a slot a row could + // already sit on; a defaulted anchor (takenAt / now) never does, so + // the probe is skipped on that path. + // + // Dose-safety guard: a TAKEN entry must not converge onto a slot + // whose anchor is in the future relative to the take. The resolver's + // forward guard already refused to snap a late take onto a future + // slot; the probe must honour the same invariant rather than binding + // the take forward onto that slot's pending REMINDER row (a + // late-morning dose consuming the evening slot). It records + // standalone (ad-hoc) instead. + const existingSlotRow = + !heldByAnotherDose && + entry.scheduledFor !== undefined && + mayConvergeOntoSuppliedSlot({ + skipped: entry.skipped, + takenAt: entry.takenAt ?? null, + suppliedSlot: incomingScheduledFor, + }) + ? await prisma.medicationIntakeEvent.findFirst({ + where: { + userId: user.id, + medicationId: entry.medicationId, + scheduledFor: incomingScheduledFor, + deletedAt: null, + }, + select: { id: true }, + }) + : null; + if (existingSlotRow) { + await writeOntoSlot(write, state, incomingScheduledFor); + return incomingScheduledFor; + } + return insertStandalone(write, state); +} diff --git a/src/lib/medications/intake-bulk/entry.ts b/src/lib/medications/intake-bulk/entry.ts new file mode 100644 index 000000000..c38998ceb --- /dev/null +++ b/src/lib/medications/intake-bulk/entry.ts @@ -0,0 +1,303 @@ +/** + * One entry of a bulk intake batch, start to end: the per-entry refusals, + * the slot attribution, the injection site, the write, and what the batch + * remembers of a row that landed. One bad entry never fails the batch; every + * refusal is that entry's own outcome. + */ +import type { Prisma } from "@/generated/prisma/client"; +import { prisma } from "@/lib/db"; +import type { SessionUser } from "@/lib/auth/session-user"; +import { dayKeyForScheduledFor } from "@/lib/medications/dose-day-refresh"; +import { resolveInjectionSiteForWrite } from "@/lib/medications/injection-site-write"; +import type { InjectionSiteKey } from "@/lib/medications/injection-sites"; +import { + INTAKE_NOT_TRACKED_REASON, + isRecordOnly, +} from "@/lib/medications/intake-tracking"; +import { + classifyExternalId, + UNSTABLE_EXTERNAL_ID_REASON, +} from "@/lib/validations/external-id"; +import type { InjectionSiteValue } from "@/lib/validations/medication"; +import { + skipEntry, + type BatchState, + type BulkIntakeEntry, +} from "./batch-state"; +import { attributeEntry } from "./entry-attribution"; +import { writeEntry } from "./entry-write"; + +/** + * What the batch reads once about the medications it names: the + * injection-tracking fields, so a per-entry `injectionSite` can be resolved + + * validated without an extra per-entry read, and `externalSource`, so the + * Apple-entry mirror gate needs no extra read either. + */ +export const OWNED_MEDICATION_SELECT = { + id: true, + deliveryForm: true, + trackInjectionSites: true, + allowedInjectionSites: true, + externalSource: true, + trackIntake: true, +} satisfies Prisma.MedicationSelect; + +export type OwnedMedication = Prisma.MedicationGetPayload<{ + select: typeof OWNED_MEDICATION_SELECT; +}>; + +export interface BatchContext { + user: Pick; + ownedSet: Set; + medById: Map; + globalExcluded: InjectionSiteValue[]; +} + +/** + * The refusals decided before any slot is read. True when the entry is + * answered (skipped, or a duplicate of a row the batch already knows). + */ +function screenEntry( + ctx: BatchContext, + state: BatchState, + entry: BulkIntakeEntry, + i: number, +): boolean { + if (!ctx.ownedSet.has(entry.medicationId)) { + skipEntry(state, i, "medication_not_found"); + return true; + } + + // A record-only medication takes no new dose (iOS #116, item 15). The + // single-dose routes answer 422 `medication.intake.notTracked`; here the + // same decision is one entry's outcome, so the rest of the batch lands. + if (isRecordOnly(ctx.medById.get(entry.medicationId) ?? {})) { + skipEntry(state, i, INTAKE_NOT_TRACKED_REASON); + return true; + } + + // Both dedup keys on this route are only idempotent while the value + // is STABLE across client launches: `(userId, externalId)` for the + // Apple dose event, and the `@unique` `idempotency_key` column for + // the replay probe. A value that rotates per process (an + // object description carrying a memory address) never matches its + // own earlier row, so every re-sync logs the dose again. Refused per + // entry — one bad row must not stop the rest of the batch landing. + const unstable = + (entry.externalId === undefined + ? null + : classifyExternalId(entry.externalId)) ?? + (entry.idempotencyKey === undefined + ? null + : classifyExternalId(entry.idempotencyKey)); + if (unstable) { + state.unstableShapes.push(unstable); + skipEntry(state, i, UNSTABLE_EXTERNAL_ID_REASON); + return true; + } + + // Apple dose-event replay: the batch pre-check found a live + // row already carrying this external UUID (or an earlier entry in + // THIS batch inserted it). First-write-wins per Apple dose — report + // `duplicate` with the existing row id so the sync cursor advances. + if (entry.externalId !== undefined) { + const existingRowId = state.rowIdByExternalId.get(entry.externalId); + if (existingRowId !== undefined) { + state.duplicates += 1; + state.results.push({ index: i, status: "duplicate", id: existingRowId }); + return true; + } + } + return false; +} + +/** + * Resolve + validate the optional per-entry injection site. A disallowed + * site marks the entry skipped without failing the batch (per-entry + * contract); a non-injection / tracking-off / non-taken entry silently drops + * it. + */ +function resolveEntrySite( + ctx: BatchContext, + entry: BulkIntakeEntry, + isExplicitTaken: boolean, +): { site: InjectionSiteKey | null } | { reason: string } { + let resolvedInjectionSite: InjectionSiteKey | null = null; + if (entry.injectionSite !== undefined) { + const med = ctx.medById.get(entry.medicationId); + const resolution = resolveInjectionSiteForWrite({ + submitted: entry.injectionSite, + taken: isExplicitTaken, + deliveryForm: med?.deliveryForm ?? "ORAL", + trackInjectionSites: med?.trackInjectionSites ?? false, + allowedInjectionSites: (med?.allowedInjectionSites ?? + []) as InjectionSiteKey[], + globalExcludedInjectionSites: ctx.globalExcluded as InjectionSiteKey[], + }); + if (resolution.kind === "disallowed") { + return { reason: "injection_site_not_allowed" }; + } + resolvedInjectionSite = resolution.site; + } + return { site: resolvedInjectionSite }; +} + +/** What the batch remembers of a row that landed. */ +function recordLanded( + ctx: BatchContext, + state: BatchState, + entry: BulkIntakeEntry, + effectiveScheduledFor: Date, +): void { + const dayKey = dayKeyForScheduledFor( + effectiveScheduledFor, + ctx.user.timezone, + ); + state.touchedDays.set(`${entry.medicationId}|${dayKey}`, { + medicationId: entry.medicationId, + dayKey, + }); + // Collect the slot for the silent cross-device sync (#22) + // only when the row's state actually changed (inserted / updated). A + // pending echo, duplicate, or no-downgrade no-op leaves the visible + // dose state untouched, so it must not wake the user's other devices. + // Register the landed row under its external dose-event + // UUID so a repeat of the same UUID later in THIS batch dedups + // in-memory instead of tripping the partial unique. + const landed = state.results[state.results.length - 1]; + if (entry.externalId !== undefined && landed?.id !== undefined) { + state.rowIdByExternalId.set(entry.externalId, landed.id); + } + const lastStatus = state.results[state.results.length - 1]?.status; + if (lastStatus === "inserted" || lastStatus === "updated") { + const scheduledForIso = effectiveScheduledFor.toISOString(); + state.syncSlots.set(`${entry.medicationId}|${scheduledForIso}`, { + medicationId: entry.medicationId, + scheduledFor: scheduledForIso, + }); + if (!entry.skipped) state.snoozeClearIds.add(entry.medicationId); + } +} + +async function writeScreenedEntry( + ctx: BatchContext, + state: BatchState, + entry: BulkIntakeEntry, + i: number, +): Promise { + const incomingScheduledFor = + entry.scheduledFor ?? entry.takenAt ?? new Date(); + + // Classify the incoming write. An offline-sync replay echoes a + // PENDING projection (no `takenAt`, `skipped:false`) for a slot the + // user may already have actioned; that echo must NEVER clear a + // recorded `takenAt`. An explicit `takenAt` or `skipped:true` is a + // real user action and applies last-write-wins. + const isExplicitTaken = !entry.skipped && entry.takenAt !== undefined; + const isExplicitSkip = entry.skipped === true; + + const attribution = await attributeEntry( + { user: ctx.user, entry, incomingScheduledFor }, + { isExplicitTaken, isExplicitSkip }, + ); + if ("reason" in attribution) { + skipEntry(state, i, attribution.reason); + return; + } + if (attribution.heldByAnotherDose) state.extraDosesUnscheduled += 1; + + const site = resolveEntrySite(ctx, entry, isExplicitTaken); + if ("reason" in site) { + skipEntry(state, i, site.reason); + return; + } + + const effectiveScheduledFor = await writeEntry( + { + user: ctx.user, + entry, + index: i, + incomingScheduledFor, + isExplicitTaken, + isExplicitSkip, + attribution, + resolvedInjectionSite: site.site, + }, + state, + ); + if (effectiveScheduledFor === null) return; + recordLanded(ctx, state, entry, effectiveScheduledFor); +} + +/** + * P2002 = unique-constraint violation. Two shapes reach here: + * 1. an idempotencyKey collision on the unscheduled/PRN insert → + * "duplicate" (the canonical-slot path already absorbs its own + * same-slot P2002 race inside `applyCanonicalSlotWrite`); + * 2. a same-slot collision on the unscheduled insert. In both + * cases we surface the existing row id so the cursor advances. + */ +async function answerFailedEntry( + ctx: BatchContext, + state: BatchState, + entry: BulkIntakeEntry, + i: number, + err: unknown, +): Promise { + const { user } = ctx; + if ( + typeof err === "object" && + err !== null && + "code" in err && + (err as { code: string }).code === "P2002" + ) { + const existing = entry.idempotencyKey + ? await prisma.medicationIntakeEvent.findUnique({ + where: { + userId_idempotencyKey: { + userId: user.id, + idempotencyKey: entry.idempotencyKey, + }, + }, + select: { id: true }, + }) + : // An Apple entry that raced the partial + // `(user_id, external_id)` unique (concurrent batches syncing + // the same dose event) resolves to the winning live row. + entry.externalId + ? await prisma.medicationIntakeEvent.findFirst({ + where: { + userId: user.id, + externalId: entry.externalId, + deletedAt: null, + }, + select: { id: true }, + }) + : null; + state.duplicates += 1; + state.results.push({ + index: i, + status: "duplicate", + id: existing?.id, + }); + return; + } + const reason = + err instanceof Error ? err.message.slice(0, 120) : "create_failed"; + skipEntry(state, i, reason); +} + +/** Ingest one entry into the batch state. */ +export async function ingestEntry( + ctx: BatchContext, + state: BatchState, + entry: BulkIntakeEntry, + i: number, +): Promise { + if (screenEntry(ctx, state, entry, i)) return; + try { + await writeScreenedEntry(ctx, state, entry, i); + } catch (err: unknown) { + await answerFailedEntry(ctx, state, entry, i, err); + } +} diff --git a/src/lib/medications/intake-bulk/ingest.ts b/src/lib/medications/intake-bulk/ingest.ts new file mode 100644 index 000000000..30c2d4e27 --- /dev/null +++ b/src/lib/medications/intake-bulk/ingest.ts @@ -0,0 +1,321 @@ +/** + * Ingest one bulk intake batch: `POST /api/medications/intake/bulk`. + * + * The route authenticates, meters its own bulk bucket, reads the body and + * answers the size and schema refusals; the ownership read, the Apple mirror + * gate, the batch pre-reads, the per-entry loop and every tail after it run + * here, in the order they always ran. The outcome is the batch refusal the + * route answers with `apiError`, or the always-200 batch envelope. + */ +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import { auditLog } from "@/lib/auth/audit"; +import type { SessionUser } from "@/lib/auth/session-user"; +import { markSyncCheckpoint } from "@/lib/sync/checkpoint"; +import { + BACKFILL_SYNC_TRIGGER, + MEDICATION_DOSE_ARRIVAL_TYPE, + recordHealthKitTypeArrivals, + type HealthKitRequestSyncTrigger, +} from "@/lib/integrations/healthkit-type-sync"; +import { invalidateUserMedications } from "@/lib/cache/invalidate"; +import { refreshDoseDay } from "@/lib/medications/dose-day-refresh"; +import { unstableExternalIdMeta } from "@/lib/validations/external-id"; +import type { InjectionSiteValue } from "@/lib/validations/medication"; +import { queueMedicationIntakeSync } from "@/lib/notifications/medication-intake-sync"; +import { dispatchMedicationIntakeWebClearBulk } from "@/lib/notifications/web-push-clear"; +import { countOutstandingDosesToday } from "@/lib/medications/outstanding-doses"; +import { + createBatchState, + type BatchState, + type BulkIntakeEntry, +} from "./batch-state"; +import { + ingestEntry, + OWNED_MEDICATION_SELECT, + type BatchContext, +} from "./entry"; + +interface BatchInput { + user: Pick; + /** + * A narrow, single-purpose credential (a bridge, an ingest token) is no + * Apple Health sync, so it never writes the dose arrival record. + */ + scoped: boolean; + entries: BulkIntakeEntry[]; + syncTrigger: HealthKitRequestSyncTrigger | undefined; + ipAddress: string | null; + originDeviceToken: string | null; +} + +export type BulkIntakeOutcome = + | { kind: "refused"; message: string; errorCode: string } + | { kind: "ingested"; body: Record }; + +/** + * Idempotent re-sync dedup: one read collects every live row already + * carrying one of the batch's external dose-event UUIDs; the per-entry loop + * reports those entries as `duplicate` (first-write-wins per Apple dose) + * without touching the DB again. Fresh inserts register into the same map so + * an intra-batch repeat of a UUID dedups too. The partial + * `(user_id, external_id)` unique (migration 0239) is the race backstop. + */ +async function loadKnownExternalIds( + userId: string, + entries: BulkIntakeEntry[], +): Promise> { + const batchExternalIds = Array.from( + new Set( + entries + .map((e) => e.externalId) + .filter((id): id is string => id !== undefined), + ), + ); + const rowIdByExternalId = new Map(); + if (batchExternalIds.length > 0) { + const existingExternalRows = await prisma.medicationIntakeEvent.findMany({ + where: { + userId, + externalId: { in: batchExternalIds }, + deletedAt: null, + }, + select: { id: true, externalId: true }, + }); + for (const row of existingExternalRows) { + if (row.externalId) rowIdByExternalId.set(row.externalId, row.id); + } + } + return rowIdByExternalId; +} + +/** + * The user's global exclusion deny-list, loaded once for the whole batch (it + * is user-scoped, not per-medication). Only read when at least one entry + * actually carries an `injectionSite`, so the hot sync path (no site) pays + * for zero extra round-trips. + */ +async function loadGlobalExcluded( + userId: string, + entries: BulkIntakeEntry[], +): Promise { + const batchHasInjectionSite = entries.some( + (e) => e.injectionSite !== undefined, + ); + let globalExcluded: InjectionSiteValue[] = []; + if (batchHasInjectionSite) { + const userRow = await prisma.user.findUnique({ + where: { id: userId }, + select: { globalExcludedInjectionSites: true }, + }); + globalExcluded = (userRow?.globalExcludedInjectionSites ?? + []) as InjectionSiteValue[]; + } + return globalExcluded; +} + +/** The audit row and the event of the batch. */ +async function recordBatch(input: BatchInput, state: BatchState) { + const { entries } = input; + const { inserted, updated, duplicates, skipped, unstableShapes } = state; + await auditLog("medications.intake.bulk.ingest", { + userId: input.user.id, + ipAddress: input.ipAddress, + details: { + processed: entries.length, + inserted, + updated, + duplicates, + skipped: skipped.length, + }, + }); + + annotate({ + action: { name: "medications.intake.bulk.ingest" }, + meta: { + processed: entries.length, + inserted, + updated, + duplicates, + skipped: skipped.length, + extra_doses_unscheduled: state.extraDosesUnscheduled, + ...(unstableShapes.length > 0 + ? unstableExternalIdMeta("medication.intake.bulk", unstableShapes) + : {}), + }, + }); +} + +/** + * The caches, the dose days, the snooze, and the cross-device and web-push + * tails of the rows the batch landed. + */ +async function settleLandedRows(input: BatchInput, state: BatchState) { + const { user } = input; + // Bust per-user medications + compliance + achievement caches when at + // least one row landed so the next read reflects the ingested batch. + // Hard-evict: this bulk endpoint is the iOS user's INTERACTIVE intake path + // (a dose taken / skipped on the phone syncs through here), so the very + // next read must show the user their own action — a marked-stale SWR cell + // would hand back the pre-dose payload. The genuinely background writers + // (the auto-miss cron in `intake-auto-skip.ts`, slot dedup in + // `intake-slot-dedup.ts`) keep the default mark-stale at their own call + // sites. + if (state.inserted > 0 || state.updated > 0) { + invalidateUserMedications(user.id, { evict: true }); + } + + // Refresh the derived rows of each distinct `(medicationId, dayKey)` the + // batch touched. Best-effort: the refresh reports its own failures and + // never fails the ingest. + for (const { medicationId, dayKey } of state.touchedDays.values()) { + await refreshDoseDay({ + userId: user.id, + medicationId, + dayKey, + tz: user.timezone, + }); + } + + // Silent cross-device intake sync (#22). ONE queued fan-out + // for the whole batch regardless of how many slots it touched (the + // per-user coalescer folds bursts further), excluding the originating + // device (`X-Device-Id` = registered `Device.token`). APNs-only, + // best-effort, fire-and-forget: the canonical rows are already + // persisted, so a sync-push miss never affects the batch response. + // The single-intake routes reset a medication's snooze on every recorded + // take; a drained offline batch must quiet the snooze the same way, or a + // dose synced in later keeps counting as snoozed although it was taken. + if (state.snoozeClearIds.size > 0) { + await prisma.medication.updateMany({ + where: { id: { in: [...state.snoozeClearIds] }, userId: user.id }, + data: { snoozedUntil: null }, + }); + } + + if (state.syncSlots.size > 0) { + queueMedicationIntakeSync({ + userId: user.id, + originDeviceToken: input.originDeviceToken, + }); + + // PWA-only counterpart: close the still-pending dose-due + // reminders for every affected slot over Web Push and refresh the app + // badge once with the post-batch outstanding-dose count. Best-effort, + // fire-and-forget — a clear miss never affects the batch response. + const affectedSlots = Array.from(state.syncSlots.values()); + void (async () => { + const badgeCount = await countOutstandingDosesToday( + user.id, + user.timezone, + ); + await dispatchMedicationIntakeWebClearBulk({ + userId: user.id, + slots: affectedSlots, + badgeCount, + }); + })(); + } +} + +/** + * Apple Health doses get the per-type arrival record the measurement batch + * keeps for its types: when they last arrived, under which trigger, when a + * sync last brought a new or changed dose, and when a background sync last + * carried them. Entries sent with `source: "APPLE_HEALTH"` only; a dose taken + * in the app is not an Apple Health arrival. Replays answered `duplicate` + * alone are not one (#1259), the same rule as the batch route. Then the + * SyncMode checkpoint, which belongs to the push that moved the data, not to + * the `/api/sync/state` handshake that reports it. This endpoint is one of + * the three domains that handshake summarises, so an intake drain advances + * it. A `backfill` catch-up does not: it is history, not a sync. + */ +async function recordSyncArrival(input: BatchInput, state: BatchState) { + const { user, entries, syncTrigger } = input; + const doseArrival = { accepted: 0, newSamples: 0 }; + for (const result of state.results) { + if (entries[result.index]?.source !== "APPLE_HEALTH") continue; + if (result.status === "skipped") continue; + doseArrival.accepted += 1; + if (result.status !== "duplicate") doseArrival.newSamples += 1; + } + if (doseArrival.accepted > 0 && !input.scoped) { + await recordHealthKitTypeArrivals(prisma, { + userId: user.id, + counts: new Map([[MEDICATION_DOSE_ARRIVAL_TYPE, doseArrival]]), + trigger: syncTrigger ?? null, + }); + } + + if (syncTrigger !== BACKFILL_SYNC_TRIGGER) { + await markSyncCheckpoint(user.id); + } +} + +export async function ingestBulkIntake( + input: BatchInput, +): Promise { + const { user, entries } = input; + + // Up-front ownership check — the user can only POST intake events + // against medications they own. One read covers every medicationId + // in the batch. + const medicationIds = Array.from(new Set(entries.map((e) => e.medicationId))); + const ownedMedications = await prisma.medication.findMany({ + where: { id: { in: medicationIds }, userId: user.id }, + select: OWNED_MEDICATION_SELECT, + }); + const ownedSet = new Set(ownedMedications.map((m) => m.id)); + const medById = new Map(ownedMedications.map((m) => [m.id, m])); + + // Source-exclusive Apple-mirrored medications: an APPLE_HEALTH + // dose event may only attach to a medication mirrored from Apple Health + // (`Medication.externalSource === "APPLE_HEALTH"`). Targeting a native + // medication is a client contract violation (the iOS sync maps HK dose + // events onto the meds it mirrored itself), not a per-entry data + // problem, so the whole batch 422s with a stable errorCode the client + // can branch on. Unowned medicationIds keep the per-entry + // `medication_not_found` skip. + const appleMismatch = entries.some( + (e) => + e.source === "APPLE_HEALTH" && + ownedSet.has(e.medicationId) && + medById.get(e.medicationId)?.externalSource !== "APPLE_HEALTH", + ); + if (appleMismatch) { + return { + kind: "refused", + message: + "An APPLE_HEALTH entry may only target a medication mirrored from Apple Health", + errorCode: "medications.intake.bulk.apple_health_not_mirrored", + }; + } + + const state = createBatchState(await loadKnownExternalIds(user.id, entries)); + const ctx: BatchContext = { + user, + ownedSet, + medById, + globalExcluded: await loadGlobalExcluded(user.id, entries), + }; + + for (let i = 0; i < entries.length; i++) { + await ingestEntry(ctx, state, entries[i], i); + } + + await recordBatch(input, state); + await settleLandedRows(input, state); + await recordSyncArrival(input, state); + + return { + kind: "ingested", + body: { + processed: entries.length, + inserted: state.inserted, + updated: state.updated, + duplicates: state.duplicates, + skipped: state.skipped, + entries: state.results, + }, + }; +} diff --git a/src/lib/medications/intake-compliance.ts b/src/lib/medications/intake-compliance.ts new file mode 100644 index 000000000..25c74f216 --- /dev/null +++ b/src/lib/medications/intake-compliance.ts @@ -0,0 +1,45 @@ +/** + * Per-day scheduled vs taken for the last N days: + * `GET /api/medications/intake?scope=compliance`. + * + * Cached at a 15-minute TTL because daily compliance buckets are slow-moving + * (intake events trickle in, yesterday's row doesn't move). The cache key + * carries the userTz so a user who changes timezone doesn't read another + * tz's bucketing. + * + * `scheduled` is the SCHEDULE-ANCHORED expected-dose count per day (the + * canonical recurrence engine), NOT the count of logged intake rows. The old + * rollup-backed path set `scheduled = COUNT(*)` of intake rows, so the + * dashboard tile's rate (`taken / scheduled`) was ~100% across every window + * regardless of real adherence — every logged row was both numerator and + * denominator. Anchoring `scheduled` to the schedule makes the rate genuinely + * reflect taken-of-expected and lets the 7/30/90 windows diverge with partial + * adherence. + */ +import { annotate } from "@/lib/logging/context"; +import { cached, caches, type ServerCache } from "@/lib/cache/server-cache"; +import { + buildScheduleAnchoredComplianceBuckets, + type ScheduleAnchoredComplianceBucket, +} from "@/lib/analytics/schedule-anchored-compliance"; + +export async function readIntakeCompliance(input: { + userId: string; + days: number; + userTz: string; +}): Promise { + const { userId, days, userTz } = input; + const result = await cached( + caches.medicationsIntake as ServerCache, + `${userId}|compliance|${days}|${userTz}`, + () => buildScheduleAnchoredComplianceBuckets(userId, days, userTz), + annotate, + ); + + annotate({ + action: { name: "medications.intake.compliance" }, + meta: { days, count: result.length }, + }); + + return result; +} diff --git a/src/lib/medications/intake-today.ts b/src/lib/medications/intake-today.ts new file mode 100644 index 000000000..9e6459e2a --- /dev/null +++ b/src/lib/medications/intake-today.ts @@ -0,0 +1,79 @@ +/** + * Today's intake events across the record's medications: + * `GET /api/medications/intake?scope=today`. + * + * The route authenticates (READ over the `medications` section) and parses + * the query; the projection, the read and the status mapping run here. + */ +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import { startOfLocalDayInTz } from "@/lib/tz/local-day"; +import { projectTodayIntakesAndRecompute } from "@/lib/medications/scheduling/project-today-intakes"; +import { TRACKED_INTAKE_EVENT_WHERE } from "@/lib/medications/intake-tracking"; + +export async function readTodayIntakes(input: { + userId: string; + userTz: string; +}) { + const { userId, userTz } = input; + const todayStart = startOfLocalDayInTz(new Date(), userTz); + const todayEnd = new Date(todayStart.getTime() + 86_400_000); + + // Project pending intake rows for every active schedule whose window opens + // today, then idempotently backfill any missing rows. Without it the + // endpoint returned `[]` for daily meds (`schedule.daysOfWeek = null` in the + // DB) until the reminder worker entered the RED phase at the end of the + // dose window — leaving the iOS Dashboard tile + the "Erfassen" sheet empty + // for the whole morning. The shared helper mirrors the dashboard-summary + // call site so the two routes converge on the same row set. + const { projected, backfilled } = await projectTodayIntakesAndRecompute({ + userId, + userTz, + todayStart, + todayEnd, + }); + + const events = await prisma.medicationIntakeEvent.findMany({ + where: { + userId, + // Exclude tombstoned rows from the today list. + deletedAt: null, + scheduledFor: { gte: todayStart, lt: todayEnd }, + // The today list is what clients offer doses from (#1033); a + // medication with intake tracking off is not on it. + ...TRACKED_INTAKE_EVENT_WHERE, + }, + orderBy: { scheduledFor: "asc" }, + include: { medication: { select: { id: true, snoozedUntil: true } } }, + }); + + annotate({ + action: { name: "medications.intake.today" }, + meta: { + count: events.length, + projected, + backfilled, + }, + }); + + return events.map((e) => ({ + id: e.id, + medicationId: e.medicationId, + scheduledAt: e.scheduledFor.toISOString(), + takenAt: e.takenAt?.toISOString() ?? null, + status: e.skipped + ? "skipped" + : e.takenAt + ? "taken" + : // A never-acted dose the auto-miss cron flipped is a + // terminal MISS, not a perpetual "pending". (Today's freshly + // projected rows are < 24 h old so this rarely fires here, but + // a stale row surfacing in the window reads honestly.) + e.autoMissed + ? "missed" + : e.medication.snoozedUntil && e.medication.snoozedUntil > new Date() + ? "snoozed" + : "pending", + snoozedUntil: e.medication.snoozedUntil?.toISOString() ?? null, + })); +} diff --git a/src/lib/medications/list-intake-events.ts b/src/lib/medications/list-intake-events.ts new file mode 100644 index 000000000..6a58e2b11 --- /dev/null +++ b/src/lib/medications/list-intake-events.ts @@ -0,0 +1,89 @@ +/** + * One medication's intake history: `GET /api/medications/{id}/intake`. + * + * The route authenticates (READ over the `medications` section), runs the + * ownership guard and parses the query; the filter, the order, the page and + * the count are built here. + */ +import type { z } from "zod/v4"; + +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import type { listIntakeEventsSchema } from "@/lib/validations/medication"; + +export type IntakeEventListQuery = z.output; + +export async function listIntakeEvents(input: { + userId: string; + medicationId: string; + query: IntakeEventListQuery; +}) { + const { userId, medicationId: id } = input; + const { limit, offset, sortBy, sortDir, status } = input.query; + + // Translate the optional `status` filter into a Prisma + // `where` fragment. Default `status:"all"` keeps the contract + // byte-stable for the iOS Swift client and the dashboard tiles that + // were on the wire before this knob existed. The detail-page + // IntakeHistoryListV2 component opts into `status:"completed"` so + // ambiguous "missed / never confirmed" rows + // (`takenAt IS NULL AND skipped = false`) stay out of the user-facing + // table — they were the source of the v1.4.36 regression where rows + // with no takenAt rendered an "Eingenommen" chip. + const statusFilter = + status === "taken" + ? { takenAt: { not: null }, skipped: false } + : status === "skipped" + ? { skipped: true } + : status === "completed" + ? { + OR: [ + { takenAt: { not: null }, skipped: false }, + { skipped: true }, + ], + } + : {}; + // Exclude tombstoned rows from the per-medication + // intake history list + its count. + const where = { + medicationId: id, + userId, + deletedAt: null, + ...statusFilter, + }; + + // Pin NULLS LAST on the `takenAt` sort. Skipped rows + // carry `takenAt: null`; under a bare `desc` collation Postgres + // emits NULLS FIRST, floating skipped/planned rows to the top of + // the history view. Pinning them last keeps the descending order + // reading today → yesterday → … with real timestamps first. Other + // sort columns are non-null so they keep the simple shape. + // The trailing `{ id: sortDir }` is the unique tiebreaker. Scheduled + // slots share an instant by construction — a twice-daily medication puts + // every morning dose on the same `scheduledFor` — so offset paging over + // the primary key alone can repeat a row on one page and drop another. + const orderBy = + sortBy === "takenAt" + ? [ + { takenAt: { sort: sortDir, nulls: "last" as const } }, + { id: sortDir }, + ] + : [{ [sortBy]: sortDir }, { id: sortDir }]; + + const [events, total] = await Promise.all([ + prisma.medicationIntakeEvent.findMany({ + where, + orderBy, + take: limit, + skip: offset, + }), + prisma.medicationIntakeEvent.count({ where }), + ]); + + annotate({ + action: { name: "medication.intake.list" }, + meta: { medication_id: id, total, status }, + }); + + return { events, meta: { total, limit, offset } }; +} diff --git a/src/lib/medications/read-medication.ts b/src/lib/medications/read-medication.ts new file mode 100644 index 000000000..9b1c02f85 --- /dev/null +++ b/src/lib/medications/read-medication.ts @@ -0,0 +1,277 @@ +/** + * Read one medication: `GET /api/medications/{id}`. + * + * The route authenticates (READ over the `medications` section) and answers + * the 404; the fetch, the category enrichment, the server-computed next due + * instant, the stock and runway, the composition, the courses and the related + * records are resolved here, in the order they always were. `null` means the + * medication is not the record's. + */ +import type { Medication, MedicationSchedule } from "@/generated/prisma/client"; +import { prisma } from "@/lib/db"; +import { sendsMedicationReminders } from "@/lib/medications/auto-mark-taken"; +import { toIngredientDtos } from "@/lib/medications/ingredients/composition"; +import { annotate } from "@/lib/logging/context"; +import type { SessionUser } from "@/lib/auth/session-user"; +import { resolveMedicationCategories } from "@/lib/medication-category"; +import { resolveCourseFields } from "@/lib/medications/courses"; +import { loadAutoTakeRollingAnchors } from "@/lib/medications/auto-take"; +import { + computeDisplayDue, + OVERDUE_LOOKBACK_MS, + toResolvedSlotMark, +} from "@/lib/medications/scheduling/next-due"; +import { + effectiveUnitsPerDose, + estimateUnitsRunwayDays, +} from "@/lib/medications/supply-runway"; +import { serializeScheduleUnitsPerDose } from "@/lib/medications/schedule-units-dto"; +import { + dueSchedules, + isRecordOnly, + scheduleWireFields, +} from "@/lib/medications/intake-tracking"; +import { floorWhole } from "@/lib/medications/units-per-dose"; +import { resolveIntakeActionability } from "@/lib/medications/intake-actionable"; +import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; +import { buildRelated } from "@/lib/links/related"; + +type ReadUser = Pick; +type MedicationWithSchedules = Medication & { + schedules: MedicationSchedule[]; +}; + +/** + * The server-computed next due instant. Rolling cadences re-anchor on the + * latest non-skipped intake, so fetch it once (no-op for calendar cadences + * but cheap + keeps the value correct for rolling injections). + */ +async function loadDisplayDue( + user: ReadUser, + id: string, + medication: MedicationWithSchedules, +) { + const lastIntake = dueSchedules(medication).some( + (s) => s.rollingIntervalDays !== null, + ) + ? await prisma.medicationIntakeEvent.findFirst({ + // A tombstoned intake no longer anchors the rolling-interval + // next-due computation. + where: { + userId: user.id, + medicationId: id, + deletedAt: null, + takenAt: { not: null }, + }, + orderBy: { takenAt: "desc" }, + select: { takenAt: true }, + }) + : null; + // Slots the user has already acted on near now, so the next-due search + // skips them and advances to the next genuinely-open slot. Bound the read + // to [now-1d, now+2d] — the lookahead only needs the slots adjacent to now. + const now = new Date(); + // Reach back as far as the widest band tail so a long-resolved past slot + // can never resurface as "overdue". + const resolvedFrom = new Date(now.getTime() - OVERDUE_LOOKBACK_MS); + const resolvedTo = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000); + const resolvedRows = await prisma.medicationIntakeEvent.findMany({ + where: { + userId: user.id, + medicationId: id, + deletedAt: null, + scheduledFor: { gte: resolvedFrom, lte: resolvedTo }, + OR: [{ takenAt: { not: null } }, { skipped: true }, { autoMissed: true }], + }, + // `takenAt` rides along so an ad-hoc row (`scheduledFor === takenAt`) + // cannot ±6h-resolve a different slot. + select: { scheduledFor: true, takenAt: true }, + }); + // Current-era floor: the open-overdue search mints from the LIVE schedule + // rows, so it must not reach past the newest revision boundary into a + // previous era's cadence. + const latestRevision = await prisma.medicationScheduleRevision.findFirst({ + // Superseded rows are audit records — a correction may have + // shortened the era, so the boundary reads only active rows. + where: { medicationId: id, supersededByRevisionId: null }, + orderBy: { validUntil: "desc" }, + select: { validUntil: true }, + }); + // Intake tracking off (#1033): nothing is due from the stored rows, and no + // runway derives from them. + const liveSchedules = dueSchedules(medication); + const userTz = user.timezone || DEFAULT_TIMEZONE; + // Under auto-take a rolling cadence stands where the job will record next + // (see `auto-take.ts`). + const engineLastIntakeAt = await loadAutoTakeRollingAnchors(prisma, { + userId: user.id, + medicationIds: medication.autoMarkTaken ? [id] : [], + now, + userTz, + lastIntakeAt: () => lastIntake?.takenAt ?? null, + }); + const display = computeDisplayDue({ + medication: { + id: medication.id, + startsOn: medication.startsOn, + endsOn: medication.endsOn, + oneShot: medication.oneShot, + createdAt: medication.createdAt, + }, + schedules: liveSchedules, + now, + userTz, + lastIntakeAt: engineLastIntakeAt(id), + resolvedSlots: resolvedRows.map(toResolvedSlotMark), + eraStart: latestRevision?.validUntil ?? null, + }); + return { display, userTz, now }; +} + +/** + * Stock + runway on the detail wire, mirroring the list payload's semantics: + * NULL stock = inventory tracking off, 0 = the supply ran out; runwayDays via + * the slot-aware burn rate. Published resolved so no client re-derives the + * inheritance / cadence math. + */ +async function loadStock( + user: ReadUser, + id: string, + medication: MedicationWithSchedules, +) { + const [usableStock, anyItemCount] = await Promise.all([ + prisma.medicationInventoryItem.aggregate({ + where: { + userId: user.id, + medicationId: id, + state: { in: ["ACTIVE", "IN_USE"] }, + unitsRemaining: { gt: 0 }, + }, + _sum: { unitsRemaining: true }, + }), + prisma.medicationInventoryItem.count({ + where: { userId: user.id, medicationId: id }, + }), + ]); + const schedulesDto = serializeScheduleUnitsPerDose( + medication.schedules, + medication.unitsPerDose, + ); + const stockUnitsRemaining = + anyItemCount > 0 ? Number(usableStock._sum.unitsRemaining ?? 0) : null; + const stockDosesRemaining = + stockUnitsRemaining === null + ? null + : floorWhole( + stockUnitsRemaining / + effectiveUnitsPerDose( + schedulesDto, + Number(medication.unitsPerDose), + ), + ); + const runwayDays = + stockUnitsRemaining === null + ? null + : estimateUnitsRunwayDays( + stockUnitsRemaining, + isRecordOnly(medication) ? [] : schedulesDto, + Number(medication.unitsPerDose), + ); + return { schedulesDto, stockUnitsRemaining, stockDosesRemaining, runwayDays }; +} + +/** The detail answer of one medication, or null for the route's 404. */ +export async function readMedication(input: { + user: ReadUser; + grantId: string | null; + id: string; +}): Promise | null> { + const { user, grantId, id } = input; + const medication = await prisma.medication.findUnique({ + where: { id }, + include: { schedules: true }, + }); + + if (!medication || medication.userId !== user.id) { + return null; + } + + let category = "OTHER"; + let categoryLabel: string | null = null; + try { + const categories = await resolveMedicationCategories([id]); + category = categories[id]?.category ?? "OTHER"; + categoryLabel = categories[id]?.categoryLabel ?? null; + } catch { + // Category enrichment is optional + } + + const { display, userTz, now } = await loadDisplayDue(user, id, medication); + + annotate({ + action: { + name: "medication.get", + entity_type: "medication", + entity_id: id, + }, + }); + + const { schedulesDto, stockUnitsRemaining, stockDosesRemaining, runwayDays } = + await loadStock(user, id, medication); + + // The composition per unit, labelled in the account language. + const ingredientRows = await prisma.medicationIngredient.findMany({ + where: { medicationId: id, userId: user.id }, + select: { + nutrient: true, + amountPerUnit: true, + unit: true, + enteredAmount: true, + enteredUnit: true, + }, + }); + + // The courses, resolved on the server (#1024). + const courseFields = ( + await resolveCourseFields( + [medication], + now, + user.timezone || DEFAULT_TIMEZONE, + ) + ).get(id); + + return { + ...medication, + unitsPerDose: Number(medication.unitsPerDose), + // `schedules: []` plus `recordedSchedules` when intake tracking is off + // (#1033, see `scheduleWireFields`). + ...scheduleWireFields(medication.trackIntake, schedulesDto), + category, + categoryLabel, + ...courseFields, + nextDueAt: display ? display.at.toISOString() : null, + // The server's own reminder decision, published resolved. + remindersActive: sendsMedicationReminders(medication, { + now, + timeZone: userTz, + }), + nextDueOverdue: display?.overdue ?? false, + // See `resolveIntakeActionability` (#1040). + ...resolveIntakeActionability( + medication, + now, + user.timezone || DEFAULT_TIMEZONE, + ), + stockUnitsRemaining, + stockDosesRemaining, + runwayDays, + ingredients: toIngredientDtos(ingredientRows, user.locale ?? null), + // The records this medication relates to (`src/lib/links/`). + related: await buildRelated(prisma, { + userId: user.id, + grantId, + kind: "medication", + id, + }), + }; +} diff --git a/src/lib/medications/record-intake/attribution.ts b/src/lib/medications/record-intake/attribution.ts new file mode 100644 index 000000000..8a1417cc5 --- /dev/null +++ b/src/lib/medications/record-intake/attribution.ts @@ -0,0 +1,223 @@ +/** + * Which slot a dose posted to `POST /api/medications/{id}/intake` binds to, + * and the binding provenance it records. + * + * Window-band slot attribution (replacing the wide ±6h `snapToleranceMs` + * nearest-snap). The take is bound to a slot by membership in that slot's + * configurable dose window — the SAME bands the read ledger + the compliance + * % consume, so the three surfaces can never disagree. A take that lands in + * no window is ad-hoc (`canonicalSlot` null) and records as a standalone + * "taken now" row; PRN meds always insert standalone. Resolved BEFORE the + * idempotency/dedup window so a scheduled dose routes through the slot upsert + * (which is itself the dedup). + * + * - SKIP: a skip is logged against a slot deliberately (it carries no + * `takenAt` to attribute by), so it keeps the canonical `scheduledFor` + * snap that binds it to the slot's pending REMINDER row. + * - TAKEN: attribute by `takenAt` band membership. The optional + * `forceSlotInstant` pins an off-window take onto a chosen real slot + * ("diesem Slot zuordnen?"); a pin that is not a real slot is a 422. + */ +import { annotate } from "@/lib/logging/context"; +import type { SessionUser } from "@/lib/auth/session-user"; +import { + findPinConflict, + mayConvergeOntoSuppliedSlot, + resolveForcedSlotForWrite, + resolveSlotForWriteByBand, + resolveSlotInstantForWrite, +} from "@/lib/medications/scheduling/slot-upsert"; + +/** A coded 422 refusal the route answers with `apiError`. */ +export interface IntakeWriteRefusal { + kind: "refused"; + message: string; + errorCode: string; +} + +export type AttributionSource = "AUTO" | "USER_PIN"; + +export interface IntakeAttribution { + kind: "attributed"; + canonicalSlot: Date | null; + /** + * USER_PIN on the forced "diesem Slot zuordnen" path, AUTO when band + * attribution decided. Skips carry no binding decision (undefined → column + * untouched/default). + */ + attributionSource: AttributionSource | undefined; +} + +interface AttributionInput { + user: Pick; + medicationId: string; + skipped: boolean; + scheduledFor: Date | undefined; + forceSlotInstant: Date | undefined; + resolvedTakenAt: Date | null; + incomingScheduledFor: Date; +} + +async function attributeSkip(input: AttributionInput): Promise { + const { user, medicationId: id, scheduledFor, incomingScheduledFor } = input; + if (scheduledFor === undefined) { + // A slot-less deliberate skip resolves by band membership + // on the skip moment, like a take does. The defaulted-now snap only + // reached the tight reminder-grace window, so a skip tapped past it + // landed as an orphan ad-hoc row while the slot's pending REMINDER + // row stayed open to auto-miss. + const attribution = await resolveSlotForWriteByBand({ + userId: user.id, + medicationId: id, + userTz: user.timezone, + takenAt: incomingScheduledFor, + }); + return attribution.slotInstant; + } + return resolveSlotInstantForWrite({ + userId: user.id, + medicationId: id, + userTz: user.timezone, + incoming: incomingScheduledFor, + instantIsExplicit: true, + isTakenWrite: false, + }); +} + +async function attributePin( + input: AttributionInput, + forceSlotInstant: Date, +): Promise { + const { user, medicationId: id, resolvedTakenAt } = input; + const canonicalSlot = await resolveForcedSlotForWrite({ + userId: user.id, + medicationId: id, + userTz: user.timezone, + slotInstant: forceSlotInstant, + }); + if (canonicalSlot === null) { + annotate({ + action: { name: "medication.intake.force_slot.invalid" }, + meta: { medication_id: id }, + }); + return { + kind: "refused", + message: "forceSlotInstant is not a scheduled slot of this medication", + errorCode: "medications.intake.force_slot.invalid", + }; + } + // Refuse to pin onto a slot another recorded action already + // serves: the explicit-write last-write-wins rule would silently + // overwrite that dose record. The ledger UI only offers the pin for + // unserved slots, so this only fires for stale clients / raw API calls. + if ( + await findPinConflict({ + userId: user.id, + medicationId: id, + canonicalSlot, + incomingTakenAt: resolvedTakenAt ?? null, + }) + ) { + annotate({ + action: { name: "medication.intake.force_slot.occupied" }, + meta: { medication_id: id }, + }); + return { + kind: "refused", + message: "forceSlotInstant already carries a recorded dose action", + errorCode: "medications.intake.force_slot.occupied", + }; + } + return { kind: "attributed", canonicalSlot, attributionSource: "USER_PIN" }; +} + +async function attributeTake( + input: AttributionInput, +): Promise { + const { + user, + medicationId: id, + skipped, + scheduledFor, + resolvedTakenAt, + incomingScheduledFor, + } = input; + // A non-skip write on this route always carries a `takenAt` (defaulted to + // now), so `resolvedTakenAt` is non-null here; the fallback only guards the + // type. + const attribution = await resolveSlotForWriteByBand({ + userId: user.id, + medicationId: id, + userTz: user.timezone, + takenAt: resolvedTakenAt ?? incomingScheduledFor, + }); + let canonicalSlot = attribution.slotInstant; + // A band decision (slot or ad-hoc) is an AUTO binding; it also resets a + // stale USER_PIN when this write converges onto a previously-pinned row. + let attributionSource: AttributionSource = "AUTO"; + + // The client can NAME the dose it is recording, and when it does, that + // answer outranks the band. The Verlauf ledger's "Genommen" posts the + // displayed slot's own anchor, and the medication card posts its + // display-due slot; both mean "record THIS dose". Band attribution answers + // a different question — which slot the take's clock time falls into — so + // backfilling a slot from an earlier day matched no band, fell through to + // the standalone insert, and left the named slot unserved: the entry + // reverted to missed on the next read and the dose the user recorded was + // filed on the wrong day. + // + // Only a REAL slot of this medication is honoured (the same band-anchor + // validation the "diesem Slot zuordnen" pin runs), so an arbitrary client + // instant still records ad-hoc rather than parking a live row on a + // fabricated anchor. And only a slot that does not sit ahead of the take, + // by the same forward guard the convergence probe uses — a + // late-morning dose must never consume the evening slot. + if ( + scheduledFor !== undefined && + mayConvergeOntoSuppliedSlot({ + skipped, + takenAt: resolvedTakenAt, + suppliedSlot: incomingScheduledFor, + }) + ) { + const namedSlot = await resolveForcedSlotForWrite({ + userId: user.id, + medicationId: id, + userTz: user.timezone, + slotInstant: incomingScheduledFor, + }); + // Only when the naming actually decides something the clock did not. + // A dose taken inside its own window resolves to the same anchor either + // way and stays an AUTO binding, so an ordinary on-time take is + // unchanged. When the two disagree the binding rests on the user's + // choice, which is what USER_PIN records — and the read ledger binds a + // USER_PIN take by its stored anchor rather than by the clock, so a + // backfilled dose reads under the slot it was recorded for instead of + // falling back to ad-hoc. + if ( + namedSlot !== null && + namedSlot.getTime() !== canonicalSlot?.getTime() + ) { + canonicalSlot = namedSlot; + attributionSource = "USER_PIN"; + } + } + return { kind: "attributed", canonicalSlot, attributionSource }; +} + +/** The slot the dose binds to, or the coded refusal of a bad pin. */ +export async function resolveIntakeAttribution( + input: AttributionInput, +): Promise { + if (input.skipped) { + return { + kind: "attributed", + canonicalSlot: await attributeSkip(input), + attributionSource: undefined, + }; + } + if (input.forceSlotInstant !== undefined) { + return attributePin(input, input.forceSlotInstant); + } + return attributeTake(input); +} diff --git a/src/lib/medications/record-intake/record-intake.ts b/src/lib/medications/record-intake/record-intake.ts new file mode 100644 index 000000000..b87562e93 --- /dev/null +++ b/src/lib/medications/record-intake/record-intake.ts @@ -0,0 +1,310 @@ +/** + * Record one dose of one medication: `POST /api/medications/{id}/intake`. + * + * The route authenticates (WRITE over the `medications` section), runs the + * intake guard, parses the body and answers the schema refusal and the + * delegated outcome-change refusal; the injection-site check, the slot + * attribution, the replay and dedup answers, the write and every tail after + * it run here, in the order they always ran. + */ +import type { z } from "zod/v4"; + +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import { auditLog } from "@/lib/auth/audit"; +import type { SessionUser } from "@/lib/auth/session-user"; +import type { intakeSchema } from "@/lib/validations/medication"; +import { resolveInjectionSiteForWrite } from "@/lib/medications/injection-site-write"; +import type { InjectionSiteKey } from "@/lib/medications/injection-sites"; +import { + consumeForIntake, + restoreForIntake, +} from "@/lib/medications/inventory/consumption"; +import { reconcileOneShotState } from "@/lib/medications/lifecycle"; +import { invalidateUserMedications } from "@/lib/cache/invalidate"; +import { queueMedicationIntakeSync } from "@/lib/notifications/medication-intake-sync"; +import { dispatchMedicationIntakeWebClear } from "@/lib/notifications/web-push-clear"; +import { countOutstandingDosesToday } from "@/lib/medications/outstanding-doses"; +import { notifyDelegatedIntake } from "@/lib/notifications/delegated-intake"; +import { refreshDoseSlot } from "@/lib/medications/dose-day-refresh"; +import { + resolveIntakeAttribution, + type IntakeWriteRefusal, +} from "./attribution"; +import { + findReplayedIntake, + writeIntake, + type IntakeWriteResult, +} from "./write"; + +export type IntakeInput = z.output; + +type WrittenIntake = Extract; + +export type RecordIntakeOutcome = + | IntakeWriteRefusal + | Exclude + | { kind: "recorded"; event: WrittenIntake["event"] }; + +interface RecordIntakeContext { + user: Pick; + actorId: string; + medicationId: string; + intakeSource: "API" | "WEB"; + isDelegated: boolean; + ipAddress: string | null; + originDeviceToken: string | null; +} + +/** + * Resolve + server-validate the optional injection site. Load the + * medication's delivery form + tracking opt-in + per-medication allowed + * sites, plus the user's global exclusion deny-list. A site outside the + * effective allowed set is a hard 422; a site on a non-injection / + * tracking-off med (or a skip) is silently dropped. + */ +async function resolveInjectionSite( + ctx: RecordIntakeContext, + data: IntakeInput, +): Promise< + { kind: "site"; site: InjectionSiteKey | null } | IntakeWriteRefusal +> { + const { user, medicationId: id } = ctx; + const { injectionSite, skipped } = data; + let resolvedInjectionSite: InjectionSiteKey | null = null; + if (injectionSite !== undefined) { + const [med, userRow] = await Promise.all([ + prisma.medication.findUnique({ + where: { id }, + select: { + deliveryForm: true, + trackInjectionSites: true, + allowedInjectionSites: true, + }, + }), + prisma.user.findUnique({ + where: { id: user.id }, + select: { globalExcludedInjectionSites: true }, + }), + ]); + const resolution = resolveInjectionSiteForWrite({ + submitted: injectionSite, + taken: !skipped, + deliveryForm: med?.deliveryForm ?? "ORAL", + trackInjectionSites: med?.trackInjectionSites ?? false, + allowedInjectionSites: (med?.allowedInjectionSites ?? + []) as InjectionSiteKey[], + globalExcludedInjectionSites: (userRow?.globalExcludedInjectionSites ?? + []) as InjectionSiteKey[], + }); + if (resolution.kind === "disallowed") { + annotate({ + action: { name: "medication.intake.injection_site.disallowed" }, + meta: { medication_id: id, site: resolution.site }, + }); + return { + kind: "refused", + message: "Injection site is not allowed for this medication", + errorCode: "medications.intake.injection_site.disallowed", + }; + } + resolvedInjectionSite = resolution.site; + } + return { kind: "site", site: resolvedInjectionSite }; +} + +/** + * Inventory consumption / restore. A taken write consumes `unitsPerDose` + * units (the stamp on the event row makes replays and re-posts exactly-once; + * `consumedTransition` skips the read for the no-op echo cases the slot + * upsert already classified). An explicit skip can downgrade a + * previously-taken row (last-write-wins), so it refunds whatever that row's + * stamp recorded. Both hooks are best-effort and never block the intake + * write. + */ +async function settleInventory( + ctx: RecordIntakeContext, + skipped: boolean, + written: WrittenIntake, +): Promise { + const { user, medicationId: id } = ctx; + const { event, consumedTransition } = written; + if (!skipped && consumedTransition) { + await consumeForIntake({ + client: prisma, + userId: user.id, + medicationId: id, + eventId: event.id, + intakeAt: event.takenAt ?? event.scheduledFor, + }); + } else if (skipped) { + await restoreForIntake({ + client: prisma, + userId: user.id, + eventId: event.id, + }); + } +} + +/** The audit row, the event, the caches, the rollup and the lifecycle. */ +async function recordAfterWrite( + ctx: RecordIntakeContext, + skipped: boolean, + event: WrittenIntake["event"], +): Promise { + const { user, medicationId: id } = ctx; + await auditLog("medication.intake", { + userId: user.id, + ipAddress: ctx.ipAddress, + details: { + medicationId: id, + eventId: event.id, + skipped, + }, + }); + + annotate({ + action: { + name: "medication.intake", + entity_type: "intake_event", + entity_id: event.id, + }, + meta: { + medication_id: id, + skipped, + }, + }); + + // Bust per-user medications + compliance + achievement + // caches so the next read reflects the dose event. + invalidateUserMedications(user.id, { evict: true }); + + // Refresh the persistent compliance rollup for the + // affected day. The hook is best-effort; failures annotate but never + // block the user's POST response. + await refreshDoseSlot({ + userId: user.id, + medicationId: id, + scheduledFor: event.scheduledFor, + tz: user.timezone, + }); + + // One-shot lifecycle reconciliation. A `oneShot` medication + // has at most one live intake; the helper re-reads the most recent + // non-skipped intake and flips `active` to match. Idempotent on + // non-one-shot medications (the underlying updateMany is gated by + // `oneShot:true`). The flip runs AFTER the intake row is committed + // so a flaky write never deactivates a medication that didn't + // actually receive its dose. + const reconcileAction = await reconcileOneShotState(prisma, id, user.id); + if (reconcileAction !== "noop") { + invalidateUserMedications(user.id, { evict: true }); + } +} + +/** The tails that never affect the response. */ +function startTails( + ctx: RecordIntakeContext, + skipped: boolean, + event: WrittenIntake["event"], +): void { + const { user, medicationId: id } = ctx; + // (#22) — silent cross-device intake sync: wake the user's OTHER iOS + // devices so a running Live Activity / widget reconciles this dose. + // Coalesced per user, best-effort — never affects the response. + queueMedicationIntakeSync({ + userId: user.id, + originDeviceToken: ctx.originDeviceToken, + }); + + // PWA counterpart of the sync wake above: a dose resolved here (taken or + // skipped) closes the still-pending dose-due Web Push reminder for the + // slot and refreshes the app badge. The other intake routes have carried + // this since v1.18.4; this route is the replay target of the iOS offline + // queue, so a drained dose must clear the web reminder the same way. + // Best-effort, fire-and-forget — the canonical row is already persisted. + void (async () => { + const badgeCount = await countOutstandingDosesToday(user.id, user.timezone); + await dispatchMedicationIntakeWebClear({ + userId: user.id, + medicationId: id, + scheduledFor: event.scheduledFor.toISOString(), + badgeCount, + }); + })(); + + // "Somebody else marked your dose". This route has no snooze arm, + // so the state is whichever of the two markings the payload carried. The + // helper refuses on self, so a person marking their own dose is unaffected. + // Fire and forget, like every neighbour above. The dispatcher awaits each + // channel in the cascade, so an unreachable one would have added its whole + // timeout to the caregiver's request — the person standing next to the + // patient waiting for a dose to register. The helper never throws, and the + // owner learning a moment later is the right trade against the tick being + // slow. The integration test already polls the ledger rather than relying on + // the await, so nothing about the proof weakens. + void notifyDelegatedIntake({ + ownerId: user.id, + actorId: ctx.actorId, + medicationId: id, + status: skipped ? "skipped" : "taken", + }); +} + +/** Record the dose the route parsed. */ +export async function recordIntake( + ctx: RecordIntakeContext, + data: IntakeInput, +): Promise { + const { user, medicationId: id } = ctx; + const { scheduledFor, takenAt, skipped, idempotencyKey, doseTaken } = data; + + // The dose override documents a consumed dose, so it only + // applies to a taken (non-skipped) write; on a skip it is silently + // dropped (the slot consumed nothing). + const resolvedDoseTaken = !skipped && doseTaken ? doseTaken : null; + + const site = await resolveInjectionSite(ctx, data); + if (site.kind === "refused") return site; + + const resolvedTakenAt = skipped ? null : (takenAt ?? new Date()); + const incomingScheduledFor = scheduledFor ?? takenAt ?? new Date(); + + const attribution = await resolveIntakeAttribution({ + user, + medicationId: id, + skipped, + scheduledFor, + forceSlotInstant: data.forceSlotInstant, + resolvedTakenAt, + incomingScheduledFor, + }); + if (attribution.kind === "refused") return attribution; + + const write = { + userId: user.id, + medicationId: id, + canonicalSlot: attribution.canonicalSlot, + attributionSource: attribution.attributionSource, + scheduledFor, + skipped, + idempotencyKey, + resolvedTakenAt, + incomingScheduledFor, + intakeSource: ctx.intakeSource, + isDelegated: ctx.isDelegated, + resolvedInjectionSite: site.site, + resolvedDoseTaken, + }; + const replayed = await findReplayedIntake(write); + if (replayed) return { kind: "existing", row: replayed }; + + const written = await writeIntake(write); + if (written.kind !== "written") return written; + + await settleInventory(ctx, skipped, written); + await recordAfterWrite(ctx, skipped, written.event); + startTails(ctx, skipped, written.event); + + return { kind: "recorded", event: written.event }; +} diff --git a/src/lib/medications/record-intake/write.ts b/src/lib/medications/record-intake/write.ts new file mode 100644 index 000000000..4db4ca049 --- /dev/null +++ b/src/lib/medications/record-intake/write.ts @@ -0,0 +1,238 @@ +/** + * The row a dose posted to `POST /api/medications/{id}/intake` lands on: the + * replay and dedup answers, the canonical slot write, the convergence probe + * onto a row already on the named instant, or the standalone insert. + */ +import type { MedicationIntakeEvent } from "@/generated/prisma/client"; +import { prisma } from "@/lib/db"; +import type { InjectionSiteKey } from "@/lib/medications/injection-sites"; +import { + applyCanonicalSlotWrite, + mayConvergeOntoSuppliedSlot, + type ApplyCanonicalSlotWriteResult, +} from "@/lib/medications/scheduling/slot-upsert"; +import type { AttributionSource } from "./attribution"; + +type IntakeRow = MedicationIntakeEvent | ApplyCanonicalSlotWriteResult["row"]; + +export interface IntakeWrite { + userId: string; + medicationId: string; + canonicalSlot: Date | null; + attributionSource: AttributionSource | undefined; + scheduledFor: Date | undefined; + skipped: boolean; + idempotencyKey: string | undefined; + resolvedTakenAt: Date | null; + incomingScheduledFor: Date; + intakeSource: "API" | "WEB"; + isDelegated: boolean; + resolvedInjectionSite: InjectionSiteKey | null; + resolvedDoseTaken: string | null; +} + +export type IntakeWriteResult = + | { kind: "existing"; row: MedicationIntakeEvent } + | { kind: "outcome_change_refused" } + | { kind: "written"; event: IntakeRow; consumedTransition: boolean }; + +/** + * Idempotency check (explicit key or server-side dedup window). The row a + * replay or a double-log answers with, or null when the dose is new. + */ +export async function findReplayedIntake( + write: IntakeWrite, +): Promise { + const { userId, medicationId: id, idempotencyKey, skipped } = write; + if (idempotencyKey) { + const existing = await prisma.medicationIntakeEvent.findFirst({ + where: { + idempotencyKey, + userId, + medicationId: id, + }, + }); + if (existing) { + return existing; + } + } else if (!write.canonicalSlot) { + // Unscheduled / PRN only — the slot upsert handles dedup for + // scheduled doses by collapsing onto the canonical slot row. + // Server-side dedup: prevent double-logging within 60 seconds. + const recentDuplicate = await prisma.medicationIntakeEvent.findFirst({ + where: { + userId, + medicationId: id, + skipped, + createdAt: { gte: new Date(Date.now() - 60_000) }, + }, + orderBy: { createdAt: "desc" }, + }); + if (recentDuplicate) { + return recentDuplicate; + } + } + return null; +} + +/** + * Converge onto the one slot row through the shared upsert: H1 deterministic + * selection, C2 no-downgrade guard, and a C1 race-safe create that re-finds + + * updates on a P2002 collision rather than 500-ing or duplicating. + */ +async function writeOntoSlot( + write: IntakeWrite, + canonicalSlot: Date, +): Promise { + const { userId, medicationId: id, skipped } = write; + // The per-med route only ever carries an explicit user gesture: + // `resolvedTakenAt` is `now()` for any non-skip POST (there is no + // "mark pending" write on this route), so a non-skip is an explicit + // taken and a `skipped:true` body is an explicit skip. Neither is a + // pending projection echo, so the no-downgrade guard never trips here — + // it is the bulk/sync route that replays pending echoes. The flags are + // threaded through so the shared upsert applies last-write-wins. + const isExplicitTaken = !skipped; + const isExplicitSkip = skipped === true; + const applied = await applyCanonicalSlotWrite({ + client: prisma, + userId, + medicationId: id, + canonicalSlot, + takenAt: write.resolvedTakenAt, + skipped, + isExplicitTaken, + isExplicitSkip, + idempotencyKey: write.idempotencyKey ?? null, + createSource: write.intakeSource, + refuseOutcomeChange: write.isDelegated, + // Resolved + validated site (null unless a tracking-on + // injection taken write supplied an allowed site). + injectionSite: write.resolvedInjectionSite, + attributionSource: write.attributionSource, + // Per-intake dose override (null unless a taken write + // carried one). + doseTaken: write.resolvedDoseTaken, + }); + if (applied.outcomeChangeRefused) return { kind: "outcome_change_refused" }; + // Reset the snooze when a dose is actually recorded (not on a + // no-downgrade no-op, which left the prior taken row untouched). + if (!skipped && !applied.noDowngradeNoOp) { + await prisma.medication.update({ + where: { id }, + data: { snoozedUntil: null }, + }); + } + return { + kind: "written", + event: applied.row, + consumedTransition: applied.consumedTransition, + }; +} + +/** + * Genuinely standalone. Anchor a taken write on the intake instant — the + * documented ad-hoc contract (`scheduledFor = takenAt`) — so an unresolvable + * client anchor can never park a live row exactly on a slot instant a pending + * REMINDER row is minted for later. A skip without a slot keeps the incoming + * instant (it has no takenAt). + */ +async function insertStandalone(write: IntakeWrite): Promise { + const { + userId, + medicationId: id, + skipped, + resolvedTakenAt, + resolvedInjectionSite, + resolvedDoseTaken, + } = write; + const [event] = await prisma.$transaction([ + prisma.medicationIntakeEvent.create({ + data: { + userId, + medicationId: id, + scheduledFor: resolvedTakenAt ?? write.incomingScheduledFor, + takenAt: resolvedTakenAt, + skipped, + source: write.intakeSource, + idempotencyKey: write.idempotencyKey ?? null, + // Site only on a resolved taken-injection write. + ...(resolvedInjectionSite !== null && { + injectionSite: resolvedInjectionSite, + }), + // Dose override only on a taken write carrying one. + ...(resolvedDoseTaken !== null && { + doseTaken: resolvedDoseTaken, + }), + }, + }), + // Reset snooze when medication is taken + ...(!skipped + ? [ + prisma.medication.update({ + where: { id }, + data: { snoozedUntil: null }, + }), + ] + : []), + ]); + return event; +} + +/** Write the dose; the caller already answered a replay. */ +export async function writeIntake( + write: IntakeWrite, +): Promise { + const { userId, medicationId: id, skipped, scheduledFor } = write; + if (write.canonicalSlot) { + // Scheduled dose — converge onto the one canonical slot row regardless + // of `source` (the pending REMINDER row, or any prior row for this + // slot). + return writeOntoSlot(write, write.canonicalSlot); + } + // Unscheduled / PRN / off-slot. When the client named an explicit + // `scheduledFor`, converge source-agnostically onto any live row that + // already sits on that instant (e.g. the pending REMINDER row the + // worker minted on a slot the band attribution did not claim) before + // inserting. Without the probe the insert lands a second live row for + // the same slot that differs only by `source` — the partial unique + // index carries `source` and cannot catch it — inflating the + // compliance rollup's scheduled count. A defaulted anchor (takenAt / + // now) never names a slot, so the probe is skipped on that hot path. + // + // Dose-safety guard: a TAKEN write must not converge onto a slot whose + // anchor is in the future relative to the take. The card advances its + // display-due to the evening slot once the morning slot's catch-up + // window lapses, so a late-morning "Genommen" posts the evening slot as + // `scheduledFor`; band attribution already rejected the take, and the + // probe must not re-bind it forward onto the evening pending row (a + // late-morning dose silently consuming the 21:00 slot). It records + // standalone (ad-hoc) instead. + const existingSlotRow = + scheduledFor !== undefined && + mayConvergeOntoSuppliedSlot({ + skipped, + takenAt: write.resolvedTakenAt, + suppliedSlot: write.incomingScheduledFor, + }) + ? await prisma.medicationIntakeEvent.findFirst({ + where: { + userId, + medicationId: id, + scheduledFor: write.incomingScheduledFor, + deletedAt: null, + }, + select: { id: true }, + }) + : null; + if (existingSlotRow) { + return writeOntoSlot(write, write.incomingScheduledFor); + } + // For the unscheduled/PRN branch a non-skip write always records a fresh + // dose, so it consumes when not skipped. + return { + kind: "written", + event: await insertStandalone(write), + consumedTransition: !skipped, + }; +} diff --git a/src/lib/medications/update-intake-status/admission.ts b/src/lib/medications/update-intake-status/admission.ts new file mode 100644 index 000000000..9eb840058 --- /dev/null +++ b/src/lib/medications/update-intake-status/admission.ts @@ -0,0 +1,188 @@ +/** + * Whether a status update to `POST /api/medications/intake` may touch the + * event it names: the live event under the record, the record-only rule, the + * delegated refusals, and the injection site it carries. + */ +import type { z } from "zod/v4"; + +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import { auditLog } from "@/lib/auth/audit"; +import { resolveInjectionSiteForWrite } from "@/lib/medications/injection-site-write"; +import { + type InjectionSiteValue, + type intakeStatusUpdateSchema, +} from "@/lib/validations/medication"; +import { isRecordOnly } from "@/lib/medications/intake-tracking"; + +export type IntakeStatusUpdate = z.output; + +/** A refusal the route answers with `apiError(message, status, meta)`. */ +export interface IntakeStatusRefusal { + kind: "refused"; + message: string; + status: number; + errorCode: string; +} + +/** + * The live event a status update may change, with what the site check needs + * of its medication, or null for the route's 404. A tombstoned intake 404s on + * a status toggle; the `deletedAt: null` filter refuses to mutate a + * soft-deleted row. + */ +export async function findIntakeForStatusUpdate(input: { + userId: string; + intakeId: string; +}) { + const existing = await prisma.medicationIntakeEvent.findFirst({ + where: { id: input.intakeId, deletedAt: null }, + include: { + medication: { + select: { + deliveryForm: true, + trackInjectionSites: true, + allowedInjectionSites: true, + trackIntake: true, + }, + }, + }, + }); + if (!existing || existing.userId !== input.userId) { + return null; + } + return existing; +} + +export type StatusUpdateEvent = NonNullable< + Awaited> +>; + +/** + * A record-only medication keeps the doses it already has, and an owner may + * still correct one of those (that is an edit of history). What it no longer + * takes is a NEW dose: resolving or snoozing an event that is still open + * would record one, and it would then be counted nowhere (iOS #116, item + * 15). Refused by name, the same code the per-medication route answers with. + */ +export function refusesUntrackedIntake(existing: StatusUpdateEvent): boolean { + const stillOpen = existing.takenAt === null && !existing.skipped; + return stillOpen && isRecordOnly(existing.medication); +} + +/** + * This route is admitted for delegates because marking a dose is the thing a + * person standing next to the patient needs to do. It is an UPDATE, though, + * and two of the transitions it can express are not that. + * + * Snoozing writes `snoozedUntil` on the OWNER's medication row, and the + * reminder cron skips a medication for as long as that stamp is in the + * future. The field is unbounded, the owner is deliberately not notified of + * a delegated snooze (there is nothing useful to say about a thirty-minute + * deferral), and the activity feed renders the whole family as "marked a + * dose". So a delegate could switch the owner's medication reminders off for + * years, and every surface that exists to make delegation visible would + * describe it as marking a dose. In an adherence product that is the worst + * thing on this route. + * + * Flipping an already-resolved event is the other one: taken to skipped and + * back rewrites the owner's compliance history and refunds inventory. The + * shipped promise, on the consent screen and in the release notes, is that + * editing what is already there stays with the owner. Marking a dose that is + * still open is a contribution; changing a decision the owner already + * recorded is an edit. + * + * Both refuse for a delegate and stay open to the owner, who is the only + * person the snooze and the correction were ever for. + */ +export async function refuseDelegatedChange(input: { + userId: string; + actorId: string; + existing: StatusUpdateEvent; + intakeId: string; + status: IntakeStatusUpdate["status"]; + ipAddress: string | null; +}): Promise { + const { userId, existing, intakeId, status } = input; + if (input.actorId !== userId) { + const alreadyResolved = existing.takenAt !== null || existing.skipped; + const refusal = + status === "snoozed" + ? "snooze" + : alreadyResolved + ? "resolved_event" + : null; + if (refusal) { + annotate({ + action: { name: "medications.intake.update.refused" }, + meta: { reason: refusal }, + }); + // Filed under the OWNER. `auditLog` stamps the actor itself from the + // request's acting context, which is what puts this on the trail the + // owner's activity panel reads — a refused attempt to change their + // record is worth as much to them as a successful one. + await auditLog("medications.intake.update.refused", { + userId, + ipAddress: input.ipAddress, + details: { intakeId, status, reason: refusal }, + }); + return { + kind: "refused", + message: + refusal === "snooze" + ? "Snoozing a reminder is not part of shared access" + : "Changing a dose that is already recorded is not part of shared access", + status: 403, + errorCode: "sharing.not_permitted", + }; + } + } + return null; +} + +/** + * Resolve + server-validate the optional injection site for a "taken" + * toggle. A site outside the medication's effective allowed set is a hard + * 422; non-injection / tracking-off / non-taken drops it. + */ +export async function resolveStatusInjectionSite(input: { + userId: string; + existing: StatusUpdateEvent; + injectionSite: IntakeStatusUpdate["injectionSite"]; + status: IntakeStatusUpdate["status"]; +}): Promise< + { kind: "site"; site: InjectionSiteValue | null } | IntakeStatusRefusal +> { + const { existing, injectionSite, status } = input; + let resolvedInjectionSite: InjectionSiteValue | null = null; + if (injectionSite !== undefined) { + const userRow = await prisma.user.findUnique({ + where: { id: input.userId }, + select: { globalExcludedInjectionSites: true }, + }); + const resolution = resolveInjectionSiteForWrite({ + submitted: injectionSite, + taken: status === "taken", + deliveryForm: existing.medication.deliveryForm, + trackInjectionSites: existing.medication.trackInjectionSites, + allowedInjectionSites: existing.medication + .allowedInjectionSites as InjectionSiteValue[], + globalExcludedInjectionSites: (userRow?.globalExcludedInjectionSites ?? + []) as InjectionSiteValue[], + }); + if (resolution.kind === "disallowed") { + annotate({ + action: { name: "medication.intake.injection_site.disallowed" }, + meta: { medication_id: existing.medicationId, site: resolution.site }, + }); + return { + kind: "refused", + message: "Injection site is not allowed for this medication", + status: 422, + errorCode: "medications.intake.injection_site.disallowed", + }; + } + resolvedInjectionSite = resolution.site; + } + return { kind: "site", site: resolvedInjectionSite }; +} diff --git a/src/lib/medications/update-intake-status/take.ts b/src/lib/medications/update-intake-status/take.ts new file mode 100644 index 000000000..5f7cec874 --- /dev/null +++ b/src/lib/medications/update-intake-status/take.ts @@ -0,0 +1,198 @@ +/** + * A "taken" status update: re-run window-band attribution so an edited / + * off-window take re-binds to the right slot instead of leaving + * `scheduledFor` stale (audit HIGH-4). `forceSlotInstant` pins onto a named + * real slot (422 if it is not one); otherwise band membership picks the slot + * (the take's own time on a miss → ad-hoc). Mirrors the per-event PUT route. + */ +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import { + applyCanonicalSlotWrite, + resolveForcedSlotForWrite, + resolveSlotForWriteByBand, +} from "@/lib/medications/scheduling/slot-upsert"; +import { + consumeForIntake, + restoreForIntake, +} from "@/lib/medications/inventory/consumption"; +import type { InjectionSiteValue } from "@/lib/validations/medication"; +import type { IntakeStatusRefusal, StatusUpdateEvent } from "./admission"; + +interface TakeInput { + userId: string; + intakeId: string; + existing: StatusUpdateEvent; + takenAt: Date | undefined; + forceSlotInstant: Date | undefined; + resolvedInjectionSite: InjectionSiteValue | null; + userTz: string; +} + +async function targetSlotOf( + input: TakeInput, + resolvedTakenAt: Date, +): Promise { + const { userId, existing, intakeId, forceSlotInstant, userTz } = input; + if (forceSlotInstant !== undefined) { + const forced = await resolveForcedSlotForWrite({ + userId, + medicationId: existing.medicationId, + userTz, + slotInstant: forceSlotInstant, + }); + if (forced === null) { + annotate({ + action: { name: "medication.intake.force_slot.invalid" }, + meta: { medication_id: existing.medicationId, intake_id: intakeId }, + }); + return { + kind: "refused", + message: "forceSlotInstant is not a scheduled slot of this medication", + status: 422, + errorCode: "medications.intake.force_slot.invalid", + }; + } + return forced; + } + const attribution = await resolveSlotForWriteByBand({ + userId, + medicationId: existing.medicationId, + userTz, + takenAt: resolvedTakenAt, + }); + return attribution.slotInstant ?? resolvedTakenAt; +} + +async function takeInPlace( + input: TakeInput, + resolvedTakenAt: Date, + wasTaken: boolean, +) { + const { userId, intakeId, existing, resolvedInjectionSite } = input; + const [updated] = await prisma.$transaction([ + prisma.medicationIntakeEvent.update({ + where: { id: intakeId }, + // Bump the reconciliation counter on every + // server-side mutation so the delta feed echoes a monotonic value. + data: { + takenAt: resolvedTakenAt, + skipped: false, + // The person confirmed it; no longer automatic. + autoRecorded: false, + syncVersion: { increment: 1 }, + // Persist the resolved site on the taken branch. + ...(resolvedInjectionSite !== null && { + injectionSite: resolvedInjectionSite, + }), + }, + }), + prisma.medication.update({ + where: { id: existing.medicationId }, + data: { snoozedUntil: null }, + }), + ]); + // The toggle recorded a take; consume inventory units + // on the genuine pending→taken (or skipped→taken) transition. The + // stamp additionally keeps a racing replay exactly-once. + if (!wasTaken) { + await consumeForIntake({ + client: prisma, + userId, + medicationId: existing.medicationId, + eventId: intakeId, + intakeAt: resolvedTakenAt, + }); + } + return updated; +} + +/** + * The take re-attributed to a different slot. Tombstone the source row and + * route the dose through the shared canonical-slot upsert, which converges + * onto any row already at the target slot rather than bare-updating into an + * occupied slot (P2002-safe). + * + * Refund the source row's consumption stamp BEFORE the tombstone, then + * consume on the converged row below: a slot move is a re-binding of one + * real dose, so the stock nets exactly one consumption. + */ +async function moveTake( + input: TakeInput, + resolvedTakenAt: Date, + targetScheduledFor: Date, + wasTaken: boolean, +) { + const { userId, intakeId, existing, resolvedInjectionSite } = input; + await restoreForIntake({ + client: prisma, + userId, + eventId: intakeId, + }); + await prisma.medicationIntakeEvent.update({ + where: { id: intakeId }, + data: { deletedAt: new Date(), syncVersion: { increment: 1 } }, + }); + const applied = await applyCanonicalSlotWrite({ + client: prisma, + userId, + medicationId: existing.medicationId, + canonicalSlot: targetScheduledFor, + takenAt: resolvedTakenAt, + skipped: false, + isExplicitTaken: true, + isExplicitSkip: false, + idempotencyKey: null, + createSource: "WEB", + // Carry the recorded site / dose override across the + // tombstone + re-insert: a slot move is a re-binding, not a new + // dose, so the original row's documentation must survive when the + // toggle write itself carries none. + injectionSite: + resolvedInjectionSite ?? + (existing.injectionSite as InjectionSiteValue | null), + doseTaken: existing.doseTaken, + }); + await prisma.medication.update({ + where: { id: existing.medicationId }, + data: { snoozedUntil: null }, + }); + // Consume on the converged row (the old row's stamp + // was refunded above, so the move nets one consumption). A taken + // source WITHOUT a stamp is a pre-v1.16.10 row whose stock moved + // through the legacy hook: the refund above was a no-op, so + // consuming on the target would double-charge — skip it. + if (!wasTaken || existing.inventoryConsumption !== null) { + await consumeForIntake({ + client: prisma, + userId, + medicationId: existing.medicationId, + eventId: applied.row.id, + intakeAt: resolvedTakenAt, + }); + } + return applied.row; +} + +/** Record the take, in place or on the slot it now binds to. */ +export async function applyTakenStatus(input: TakeInput) { + const resolvedTakenAt = input.takenAt ?? new Date(); + + const target = await targetSlotOf(input, resolvedTakenAt); + if (!(target instanceof Date)) return target; + const targetScheduledFor = target; + + const slotMoved = + targetScheduledFor.getTime() !== input.existing.scheduledFor.getTime(); + + // Transition gate for the inventory consume. Only + // a row moving INTO taken may consume; a re-take of an already- + // taken row (e.g. a time correction) leaves its stamp frozen, and a + // pre-v1.16.10 taken row (NULL stamp, stock already moved by the + // legacy hook at take time) must never retro-consume. + const wasTaken = input.existing.takenAt !== null && !input.existing.skipped; + const updated = !slotMoved + ? await takeInPlace(input, resolvedTakenAt, wasTaken) + : await moveTake(input, resolvedTakenAt, targetScheduledFor, wasTaken); + return { kind: "updated" as const, row: updated }; +} diff --git a/src/lib/medications/update-intake-status/update-intake-status.ts b/src/lib/medications/update-intake-status/update-intake-status.ts new file mode 100644 index 000000000..4263ae952 --- /dev/null +++ b/src/lib/medications/update-intake-status/update-intake-status.ts @@ -0,0 +1,250 @@ +/** + * Mark, skip or snooze one intake event: `POST /api/medications/intake`. + * + * The route authenticates (WRITE over the `medications` section), parses the + * body and answers the schema refusal and the 404; the record-only rule, the + * delegated refusals, the injection site, the status write and every tail + * after it run here, in the order they always ran. Everything downstream + * addresses the RECORD: the cross-device intake sync wakes the OWNER's iOS + * devices, the web-clear closes the OWNER's pending dose reminder and + * rewrites the OWNER's badge, and the compliance rollup recomputes the + * OWNER's day. The owner is told who marked the dose at the end. + */ +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import { auditLog } from "@/lib/auth/audit"; +import type { SessionUser } from "@/lib/auth/session-user"; +import { DEFAULT_TIMEZONE } from "@/lib/tz/resolver"; +import { invalidateUserMedications } from "@/lib/cache/invalidate"; +import { refreshDoseSlot } from "@/lib/medications/dose-day-refresh"; +import { restoreForIntake } from "@/lib/medications/inventory/consumption"; +import { queueMedicationIntakeSync } from "@/lib/notifications/medication-intake-sync"; +import { dispatchMedicationIntakeWebClear } from "@/lib/notifications/web-push-clear"; +import { notifyDelegatedIntake } from "@/lib/notifications/delegated-intake"; +import { countOutstandingDosesToday } from "@/lib/medications/outstanding-doses"; +import { + refuseDelegatedChange, + refusesUntrackedIntake, + resolveStatusInjectionSite, + type IntakeStatusRefusal, + type IntakeStatusUpdate, + type StatusUpdateEvent, +} from "./admission"; +import { applyTakenStatus } from "./take"; + +interface StatusContext { + user: Pick; + actorId: string; + existing: StatusUpdateEvent; + ipAddress: string | null; + originDeviceToken: string | null; +} + +type UpdatedRow = Extract< + Awaited>, + { kind: "updated" } +>["row"]; + +export type IntakeStatusOutcome = + | IntakeStatusRefusal + | { kind: "untracked" } + | { kind: "updated"; row: UpdatedRow | null }; + +/** The skip and the snooze; the take has its own module. */ +async function applySkipOrSnooze(ctx: StatusContext, data: IntakeStatusUpdate) { + const { user, existing } = ctx; + const { intakeId, status, snoozedUntil } = data; + if (status === "skipped") { + const updated = await prisma.medicationIntakeEvent.update({ + where: { id: intakeId }, + // Bump the reconciliation counter on the skip toggle. + // A skip the person records clears the automatic marker. + data: { + takenAt: null, + skipped: true, + autoRecorded: false, + syncVersion: { increment: 1 }, + }, + }); + // The row stopped being taken; refund whatever its + // consumption stamp recorded (no-op for a never-consumed row). + await restoreForIntake({ + client: prisma, + userId: user.id, + eventId: intakeId, + }); + return updated; + } + // snoozed: snoozedUntil lives on the Medication row. + const until = snoozedUntil ?? new Date(Date.now() + 30 * 60_000); // default +30min + await prisma.medication.update({ + where: { id: existing.medicationId }, + data: { snoozedUntil: until }, + }); + return prisma.medicationIntakeEvent.findUnique({ + where: { id: intakeId }, + }); +} + +/** + * The audit row, the event, the caches and the rollup rows of the day the + * event sat on and, after a slot move, of the day it moved to. + */ +async function recordAfterUpdate( + ctx: StatusContext, + data: IntakeStatusUpdate, + updated: UpdatedRow | null, + userTzForHook: string, +): Promise { + const { user, existing } = ctx; + const { intakeId, status } = data; + await auditLog("medications.intake.update", { + userId: user.id, + ipAddress: ctx.ipAddress, + details: { intakeId, status }, + }); + + annotate({ + action: { name: "medications.intake.update" }, + meta: { intakeId, status }, + }); + + // Bust the medications + compliance + achievement + // caches for this user so the next read reflects the dose change. + invalidateUserMedications(user.id, { evict: true }); + + // Refresh the persistent compliance rollup row for + // the affected day so the next read after the cache miss returns + // the up-to-date `(scheduled, taken, skipped)` tuple. Best-effort: + // a populator failure annotates ops without blocking the response. + await refreshDoseSlot({ + userId: user.id, + medicationId: existing.medicationId, + scheduledFor: existing.scheduledFor, + tz: userTzForHook, + }); + // A slot move re-binds the dose onto a DIFFERENT instant, + // possibly a different local day. Recompute the target day too, or the + // source day self-corrects while the destination keeps its stale tuple. + const movedTo = updated?.scheduledFor; + if (movedTo && movedTo.getTime() !== existing.scheduledFor.getTime()) { + await refreshDoseSlot({ + userId: user.id, + medicationId: existing.medicationId, + scheduledFor: movedTo, + tz: userTzForHook, + }); + } + return movedTo; +} + +/** The tails that never affect the response. */ +function startTails( + ctx: StatusContext, + status: IntakeStatusUpdate["status"], + movedTo: Date | undefined, + userTzForHook: string, +): void { + const { user, existing } = ctx; + // (#22) — silent cross-device intake sync. Wake the user's OTHER + // iOS devices so a running Live Activity / Home-Screen widget reconciles + // the dose state changed here. APNs-only, best-effort, coalesced per user: + // the canonical row is already persisted, so a sync-push miss never + // affects the response. The originating device (the one that POSTed) is + // excluded via its `X-Device-Id` (= registered `Device.token`). + queueMedicationIntakeSync({ + userId: user.id, + originDeviceToken: ctx.originDeviceToken, + }); + + // PWA-only equivalent of the Live Activity end: when a dose is + // resolved (taken / skipped), push a `type:"clear"` to the user's Web Push + // subscriptions so the still-pending dose-due reminder for this slot is + // closed by the service worker (matched on the stable slot tag) and the app + // badge re-reflects the outstanding-dose count. A snooze leaves the dose + // outstanding, so it neither clears the reminder nor changes the badge. + if (status === "taken" || status === "skipped") { + void (async () => { + const badgeCount = await countOutstandingDosesToday( + user.id, + userTzForHook, + ); + await dispatchMedicationIntakeWebClear({ + userId: user.id, + medicationId: existing.medicationId, + scheduledFor: (movedTo ?? existing.scheduledFor).toISOString(), + badgeCount, + }); + })(); + } + + // "Somebody else marked your dose". The helper refuses on self and + // on a snooze, resolves both names itself, and never throws. + // + // Fire and forget, like every neighbour above. The dispatcher awaits each + // channel in the cascade, so an unreachable one would have added its whole + // timeout to the caregiver's request — the person standing next to the + // patient waiting for a dose to register. The helper never throws, and the + // owner learning a moment later is the right trade against the tick being + // slow. The integration test already polls the ledger rather than relying on + // the await, so nothing about the proof weakens. + void notifyDelegatedIntake({ + ownerId: user.id, + actorId: ctx.actorId, + medicationId: existing.medicationId, + status, + }); +} + +/** Apply the status update to the event the route found. */ +export async function updateIntakeStatus( + ctx: StatusContext, + data: IntakeStatusUpdate, +): Promise { + const { user, existing } = ctx; + const { intakeId, status } = data; + + if (refusesUntrackedIntake(existing)) return { kind: "untracked" }; + + const refused = await refuseDelegatedChange({ + userId: user.id, + actorId: ctx.actorId, + existing, + intakeId, + status, + ipAddress: ctx.ipAddress, + }); + if (refused) return refused; + + const site = await resolveStatusInjectionSite({ + userId: user.id, + existing, + injectionSite: data.injectionSite, + status, + }); + if (site.kind === "refused") return site; + + const userTzForHook = user.timezone ?? DEFAULT_TIMEZONE; + + let updated: UpdatedRow | null; + if (status === "taken") { + const taken = await applyTakenStatus({ + userId: user.id, + intakeId, + existing, + takenAt: data.takenAt, + forceSlotInstant: data.forceSlotInstant, + resolvedInjectionSite: site.site, + userTz: userTzForHook, + }); + if (taken.kind === "refused") return taken; + updated = taken.row; + } else { + updated = await applySkipOrSnooze(ctx, data); + } + + const movedTo = await recordAfterUpdate(ctx, data, updated, userTzForHook); + startTails(ctx, status, movedTo, userTzForHook); + + return { kind: "updated", row: updated }; +} diff --git a/src/lib/medications/update-medication/preflight.ts b/src/lib/medications/update-medication/preflight.ts new file mode 100644 index 000000000..5becef569 --- /dev/null +++ b/src/lib/medications/update-medication/preflight.ts @@ -0,0 +1,273 @@ +/** + * What a medication edit checks before anything is written: the stored + * pre-image, the record-only rule, the category, the as-needed and one-shot + * shapes, the primary-schedule grace bridge and the course window. Each + * refusal leaves the medication as it was. + */ +import type { Prisma } from "@/generated/prisma/client"; +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import { + checkCurrentWindow, + courseRefusalResponse, +} from "@/lib/medications/courses"; +import type { MedicationEdit } from "./schedule-normalise"; + +/** A refusal the route answers with `apiError(message, status, meta)`. */ +export interface MedicationEditRefusal { + ok: false; + message: string; + status: number; + meta?: { errorCode: string }; +} + +const EDIT_SELECT = { + active: true, + createdAt: true, + // `asNeeded` + the schedule count feed the as-needed invariants: a flip + // to as-needed must end schedule-less, a flip back to scheduled must end + // with at least one schedule. + asNeeded: true, + // The tracking transition (#1033) needs the before. + trackIntake: true, + // `autoMarkTakenSince` is stamped only on an off→on flip. + autoMarkTaken: true, + // The pre-image the audit row carries (C4). + name: true, + dose: true, + endsOn: true, + // The course check (#1024) needs the stored window. + startsOn: true, + oneShot: true, + _count: { select: { schedules: true } }, +} satisfies Prisma.MedicationSelect; + +export type EditableMedication = Prisma.MedicationGetPayload<{ + select: typeof EDIT_SELECT; +}>; + +/** + * The narrow pre-image the edit reads after the route's ownership guard + * passed: `active` for the pausedAt derivation, and the rest for the + * invariants and the audit row. Null for the route's 404. + */ +export async function findMedicationForEdit( + id: string, +): Promise { + return prisma.medication.findUnique({ + where: { id }, + select: EDIT_SELECT, + }); +} + +/** + * ── A record-only schedule is only replaced knowingly (#1033) ── + * + * A medication with intake tracking off is served with `schedules: []` + * and its stored schedule in `recordedSchedules`, so a client that + * predates the field arms no reminder from it. The price is that such a + * client cannot SEE the stored schedule. The shipped iPhone app (1.0.3 / + * 1.0.4) opens a medication without schedule rows in its editor as + * "daily at 08:00"; a rename or dose edit sends no `schedules` (its + * editor diffs against that same prefill), but any touch of a schedule + * field sends a full replacement built from the 08:00 default plus + * `asNeeded` and `oneShot`, and never `trackIntake`. Applied, that would + * overwrite the schedule the person kept on record with one they never + * chose (or flip it to as-needed and delete the rows). + * + * Rule: on a medication whose intake tracking is off, a write that does + * not name `trackIntake` leaves the schedule shape alone. `schedules`, + * `asNeeded` and `oneShot` are dropped from it; every other field (name, + * dose, dates, `active`, `notificationsEnabled`, inventory, ...) applies + * as sent. A client that knows about the switch names it on every + * schedule save (the web editor and wizard do), and those writes apply. + */ +export function applyRecordOnlyRule( + existing: EditableMedication, + data: MedicationEdit, + id: string, +): MedicationEdit { + const keepRecordedSchedule = + existing.trackIntake === false && + data.trackIntake === undefined && + (data.schedules !== undefined || + data.asNeeded !== undefined || + data.oneShot !== undefined); + const input = keepRecordedSchedule + ? { + ...data, + schedules: undefined, + asNeeded: undefined, + oneShot: undefined, + } + : data; + if (keepRecordedSchedule) { + annotate({ + action: { + name: "medication.update.record_schedule_kept", + entity_type: "medication", + entity_id: id, + }, + meta: { + dropped_schedules: data.schedules !== undefined, + dropped_as_needed: data.asNeeded !== undefined, + dropped_one_shot: data.oneShot !== undefined, + }, + }); + } + return input; +} + +/** + * ── As-needed invariants (#316) ── + * + * An as-needed medication ends schedule-less, always. The Zod layer + * already rejects a populated `schedules` array alongside + * `asNeeded: true`; the route covers the cross-row cases the schema + * cannot see: + * - flipping an existing scheduled medication to as-needed + * requires `schedules: []` in the same request (keeping the old + * rows alongside the flag is a 422, per the feature contract); + * - flipping as-needed OFF requires the medication to end with at + * least one schedule (the wizard always sends the full array). + */ +export function refuseAsNeededShape( + existing: EditableMedication, + asNeeded: boolean | undefined, + schedules: MedicationEdit["schedules"], +): MedicationEditRefusal | null { + const effectiveAsNeeded = asNeeded ?? existing.asNeeded; + if (effectiveAsNeeded) { + const endsScheduleless = schedules + ? schedules.length === 0 + : existing._count.schedules === 0; + if (!endsScheduleless) { + return { + ok: false, + message: + "An as-needed medication cannot carry schedules (send schedules: [] to clear them)", + status: 422, + }; + } + } else if ( + asNeeded === false && + existing.asNeeded && + (!schedules || schedules.length === 0) + ) { + return { + ok: false, + message: "A scheduled medication requires at least one schedule", + status: 422, + }; + } + return null; +} + +export interface GracePatch { + scheduleId: string; + reminderGraceMinutes: number | null; +} + +/** + * ── Primary-schedule grace bridge ── + * + * The detail-page settings section saves the reminder-window in + * one PUT carrying only `{ reminderGraceMinutes }` at the top + * level (no full `schedules` array). Map it onto the medication's + * primary schedule before the Prisma update so the persisted + * shape stays per-schedule and the engine reads it from the same + * column as the wizard write-path. A full `schedules` array on + * the same request takes precedence — the wizard already declares + * the grace per row and we never want to overwrite that intent. + */ +export async function findPrimaryScheduleGracePatch( + id: string, + topLevelGraceMinutes: number | null | undefined, + schedules: MedicationEdit["schedules"], +): Promise { + let primaryScheduleGracePatch: GracePatch | null = null; + if (topLevelGraceMinutes !== undefined && !schedules) { + const primary = await prisma.medicationSchedule.findFirst({ + where: { medicationId: id }, + orderBy: { windowStart: "asc" }, + select: { id: true }, + }); + if (primary) { + primaryScheduleGracePatch = { + scheduleId: primary.id, + reminderGraceMinutes: topLevelGraceMinutes, + }; + } + } + return primaryScheduleGracePatch; +} + +/** + * ── Route invariants for the scheduling primitives ── + * + * Mirrors the create: oneShot ⇒ at-most-one schedule + no + * recurrence on it; endsOn normalised to startsOn for one-shot; + * recurring default = FREQ=DAILY when nothing else is set; + * timesOfDay dual-write from windowStart when absent. + * + * The PUT replaces all schedules wholesale, so the invariants run + * against the incoming `schedules` array (if provided) the same + * way they do on create. + */ +export function refuseOneShotShape( + oneShot: boolean | undefined, + schedules: MedicationEdit["schedules"], +): MedicationEditRefusal | null { + if (oneShot === true && schedules) { + if (schedules.length > 1) { + return { + ok: false, + message: "A one-shot medication can have at most one schedule", + status: 422, + }; + } + const s = schedules[0]; + if (s && (s.rrule !== undefined || s.rollingIntervalDays !== undefined)) { + return { + ok: false, + message: + "A one-shot medication cannot have a recurrence (rrule or rollingIntervalDays)", + status: 422, + }; + } + } + return null; +} + +/** + * The window a PUT sets is the latest course (#1024). Check it against the + * other courses BEFORE the row changes, so a refused window leaves the + * medication as it was. + */ +export async function refuseCourseWindow(args: { + userId: string; + medicationId: string; + timeZone: string; + nextWindow: { startsOn: Date | null; endsOn: Date | null }; + oneShot: boolean; + createdAt: Date; +}): Promise { + const refusal = await checkCurrentWindow({ + userId: args.userId, + medicationId: args.medicationId, + timeZone: args.timeZone, + ...args.nextWindow, + oneShot: args.oneShot, + createdAt: args.createdAt, + }); + if (refusal) { + const r = courseRefusalResponse(refusal); + return { + ok: false, + message: r.message, + status: r.status, + meta: { errorCode: r.errorCode }, + }; + } + return null; +} diff --git a/src/lib/medications/update-medication/schedule-normalise.ts b/src/lib/medications/update-medication/schedule-normalise.ts new file mode 100644 index 000000000..0d81fa66a --- /dev/null +++ b/src/lib/medications/update-medication/schedule-normalise.ts @@ -0,0 +1,157 @@ +/** + * The incoming schedules of a medication edit, normalised once. + * + * The same values feed the nested create payload AND the revision + * compare/snapshot, so the material-change gate sees exactly what will be + * persisted. The invariants mirror the create: recurring default + * `FREQ=DAILY` when nothing else is set, `timesOfDay` dual-written from + * `windowStart` when absent, and the window pulled to the dose times when a + * time falls outside it. + */ +import type { z } from "zod/v4"; + +import { serializeScheduleRecurrence } from "@/lib/medication-schedule"; +import { toRevisionPayloadEntry } from "@/lib/medications/scheduling/schedule-eras"; +import { hhmmToMinutesOrNull } from "@/lib/medications/scheduling/hhmm"; +import type { updateMedicationSchema } from "@/lib/validations/medication"; + +export type MedicationEdit = z.output; +type ScheduleInput = NonNullable[number]; + +/** Parse "HH:mm" into minutes-of-day; NaN-safe (malformed → 0). */ +function hhmmToMinutes(value: string): number { + return hhmmToMinutesOrNull(value) ?? 0; +} + +/** + * Reconcile a schedule's `windowStart` / `windowEnd` with its effective + * `timesOfDay`. The schedule editor can change only the dose times while the + * client echoes the previous window back; a time outside the window then + * breaks every consumer that assumes `timesOfDay ⊆ [windowStart, windowEnd]` + * (the window-status pill, the reminder phases, the legacy compliance + * walker). When ANY time falls outside the window — including the overnight + * `windowEnd < windowStart` shape, checked with wrap-around membership — + * the window is pulled to the min/max of the times so the persisted row is + * self-consistent. A window that already covers every time is left + * byte-identical (explicitly wider windows are a feature). + */ +function reconcileScheduleWindow( + windowStart: string, + windowEnd: string, + timesOfDay: string[], +): { windowStart: string; windowEnd: string } { + if (timesOfDay.length === 0) return { windowStart, windowEnd }; + const startMins = hhmmToMinutes(windowStart); + const endMins = hhmmToMinutes(windowEnd); + const overnight = endMins < startMins; + const inWindow = (t: number): boolean => + overnight ? t >= startMins || t <= endMins : t >= startMins && t <= endMins; + const sorted = [...timesOfDay].sort( + (a, b) => hhmmToMinutes(a) - hhmmToMinutes(b), + ); + const allInside = sorted.every((t) => inWindow(hhmmToMinutes(t))); + if (allInside) return { windowStart, windowEnd }; + return { windowStart: sorted[0], windowEnd: sorted[sorted.length - 1] }; +} + +function normaliseSchedule(s: ScheduleInput, oneShot: boolean | undefined) { + // Invariant 2 — default to FREQ=DAILY when nothing else is set. + // PRN carries no cadence, so never default it. + const hasLegacyDays = (s.daysOfWeek?.length ?? 0) > 0; + const isPrn = s.scheduleType === "PRN"; + const defaultedRrule = + oneShot !== true && + !isPrn && + s.rrule === undefined && + s.rollingIntervalDays === undefined && + !hasLegacyDays + ? "FREQ=DAILY" + : s.rrule; + + // Invariant 3 — dual-write timesOfDay from windowStart. + const effectiveTimesOfDay = + s.timesOfDay && s.timesOfDay.length > 0 ? s.timesOfDay : [s.windowStart]; + + // Invariant 4 — window / times consistency. See + // `reconcileScheduleWindow`: a times-only edit that echoes the + // stale window back gets the window pulled to the min/max of + // the new times. + const window = reconcileScheduleWindow( + s.windowStart, + s.windowEnd, + effectiveTimesOfDay, + ); + + const serializedDaysOfWeek = serializeScheduleRecurrence({ + daysOfWeek: s.daysOfWeek ?? [], + intervalWeeks: s.intervalWeeks ?? 1, + }); + + return { + createData: { + windowStart: window.windowStart, + windowEnd: window.windowEnd, + label: s.label ?? null, + dose: s.dose ?? null, + // #219 — per-schedule inventory units, field-by-field. A schedule + // replace re-creates the rows, so absent leaves the column NULL + // and the consume hook inherits the medication-level value. + ...(s.unitsPerDose !== undefined && { + unitsPerDose: s.unitsPerDose, + }), + daysOfWeek: serializedDaysOfWeek, + // First-class times-of-day. + timesOfDay: effectiveTimesOfDay, + ...(s.reminderGraceMinutes !== undefined && { + reminderGraceMinutes: s.reminderGraceMinutes, + }), + ...(defaultedRrule !== undefined && { rrule: defaultedRrule }), + ...(s.rollingIntervalDays !== undefined && { + rollingIntervalDays: s.rollingIntervalDays, + }), + // Schedule type + cyclic weeks, field-by-field. + ...(s.scheduleType !== undefined && { + scheduleType: s.scheduleType, + }), + ...(s.cyclicOnWeeks !== undefined && { + cyclicOnWeeks: s.cyclicOnWeeks, + }), + ...(s.cyclicOffWeeks !== undefined && { + cyclicOffWeeks: s.cyclicOffWeeks, + }), + // Per-dose configurable on-time windows. A `schedules` + // replace re-creates the rows, so the column is re-written each + // time; absent leaves it NULL (default ±1h derivation). + ...(s.doseWindows !== undefined && { + doseWindows: s.doseWindows, + }), + }, + // Mirror of the row the create above will persist (defaults applied), + // shaped for the cadence compare. + snapshot: toRevisionPayloadEntry({ + timesOfDay: effectiveTimesOfDay, + windowStart: window.windowStart, + windowEnd: window.windowEnd, + daysOfWeek: serializedDaysOfWeek, + rrule: defaultedRrule ?? null, + rollingIntervalDays: s.rollingIntervalDays ?? null, + scheduleType: s.scheduleType ?? "SCHEDULED", + cyclicOnWeeks: s.cyclicOnWeeks ?? null, + cyclicOffWeeks: s.cyclicOffWeeks ?? null, + doseWindows: s.doseWindows ?? null, + label: s.label ?? null, + dose: s.dose ?? null, + reminderGraceMinutes: s.reminderGraceMinutes ?? null, + }), + }; +} + +export type NormalisedSchedule = ReturnType; + +/** The incoming schedules, normalised; undefined when the edit sends none. */ +export function normaliseSchedules( + schedules: MedicationEdit["schedules"], + oneShot: boolean | undefined, +): NormalisedSchedule[] | undefined { + return schedules?.map((s) => normaliseSchedule(s, oneShot)); +} diff --git a/src/lib/medications/update-medication/update-medication.ts b/src/lib/medications/update-medication/update-medication.ts new file mode 100644 index 000000000..be7c9ba20 --- /dev/null +++ b/src/lib/medications/update-medication/update-medication.ts @@ -0,0 +1,387 @@ +/** + * Edit one medication: `PUT /api/medications/{id}`. + * + * The route authenticates (MANAGE over the `medications` section), runs the + * ownership guard, reads the pre-image through `findMedicationForEdit` and + * parses the body; the record-only rule, the invariants, the transaction and + * everything after it run here, in the order they always ran. The outcome is + * a refusal the route answers with `apiError`, or the edited medication. + */ +import { prisma } from "@/lib/db"; +import { + enqueueSupplementNutrientRecompute, + medicationHasComposition, +} from "@/lib/medications/ingredients/recompute-job"; +import { annotate } from "@/lib/logging/context"; +import { auditLog } from "@/lib/auth/audit"; +import type { SessionUser } from "@/lib/auth/session-user"; +import { overwriteDetails } from "@/lib/sharing/audit-details"; +import { invalidateUserMedications } from "@/lib/cache/invalidate"; +import { + isAssignableCategory, + resolveMedicationCategories, + setMedicationCategory, +} from "@/lib/medication-category"; +import { + CourseWriteError, + courseRefusalResponse, + resolveCourseFields, +} from "@/lib/medications/courses"; +import { refreshDoseDay } from "@/lib/medications/dose-day-refresh"; +import { serializeScheduleUnitsPerDose } from "@/lib/medications/schedule-units-dto"; +import { scheduleWireFields } from "@/lib/medications/intake-tracking"; +import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; +import { + applyRecordOnlyRule, + findPrimaryScheduleGracePatch, + refuseAsNeededShape, + refuseCourseWindow, + refuseOneShotShape, + type EditableMedication, + type GracePatch, + type MedicationEditRefusal, +} from "./preflight"; +import { normaliseSchedules, type MedicationEdit } from "./schedule-normalise"; +import { + createEraTrail, + writeMedicationEdit, + type EraTrail, + type MedicationEditWrite, +} from "./write"; + +type EditUser = Pick; +type EditedMedication = Awaited>; + +export type MedicationEditOutcome = + MedicationEditRefusal | { ok: true; body: Record }; + +/** + * Everything the edit checks and derives before the transaction, in the + * order the route always ran it. A refusal leaves the medication as it was. + */ +async function planMedicationEdit(input: { + user: EditUser; + id: string; + existing: EditableMedication; + data: MedicationEdit; +}): Promise< + | MedicationEditRefusal + | { + ok: true; + write: MedicationEditWrite; + primaryScheduleGracePatch: GracePatch | null; + } +> { + const { user, id, existing } = input; + const edit = applyRecordOnlyRule(existing, input.data, id); + const { + category, + active, + schedules, + startsOn, + endsOn, + oneShot, + asNeeded, + trackIntake, + reminderGraceMinutes: topLevelGraceMinutes, + } = edit; + + // A custom category must be one of this record's own (#1041). + if ( + category !== undefined && + !(await isAssignableCategory(user.id, category)) + ) { + return { + ok: false, + message: "Unknown medication category", + status: 422, + meta: { errorCode: "medications.category.unknown" }, + }; + } + + // Intake tracking transition (#1033). Off keeps the schedule as a record; + // the live era from here on expects nothing. + const wasTracked = existing.trackIntake !== false; + const willTrack = trackIntake ?? wasTracked; + const trackingChanged = wasTracked !== willTrack; + + const asNeededRefusal = refuseAsNeededShape(existing, asNeeded, schedules); + if (asNeededRefusal) return asNeededRefusal; + + const primaryScheduleGracePatch = await findPrimaryScheduleGracePatch( + id, + topLevelGraceMinutes, + schedules, + ); + + const oneShotRefusal = refuseOneShotShape(oneShot, schedules); + if (oneShotRefusal) return oneShotRefusal; + + // Normalise endsOn for one-shot. `oneShot === true` + `startsOn` + // means the dose is the start date; endsOn auto-matches. + const normalisedEndsOn = oneShot === true && startsOn ? startsOn : endsOn; + + const touchesWindow = + startsOn !== undefined || + normalisedEndsOn !== undefined || + oneShot !== undefined; + const nextWindow = { + startsOn: startsOn !== undefined ? startsOn : existing.startsOn, + endsOn: normalisedEndsOn !== undefined ? normalisedEndsOn : existing.endsOn, + }; + const courseTz = user.timezone || DEFAULT_TIMEZONE; + if (touchesWindow) { + const refusal = await refuseCourseWindow({ + userId: user.id, + medicationId: id, + timeZone: courseTz, + nextWindow, + oneShot: oneShot ?? existing.oneShot, + createdAt: existing.createdAt, + }); + if (refusal) return refusal; + } + + const pausedAtPatch = + active === undefined + ? {} + : active + ? { pausedAt: null as Date | null } + : existing.active + ? { pausedAt: new Date() } + : {}; + + return { + ok: true, + primaryScheduleGracePatch, + write: { + user, + id, + existing, + input: edit, + normalisedSchedules: normaliseSchedules(schedules, oneShot), + wasTracked, + willTrack, + trackingChanged, + pausedAtPatch, + touchesWindow, + nextWindow, + courseTz, + trail: createEraTrail(), + }, + }; +} + +/** + * Refresh each dose day the tombstoned slots sat on, now that the tombstones + * are committed, then apply the primary-schedule grace bridge. Both run + * after the transaction so a failure here doesn't roll the edit back. + */ +async function settleAfterCommit( + write: MedicationEditWrite, + medication: EditedMedication, + primaryScheduleGracePatch: GracePatch | null, +): Promise { + const { user, id, trail } = write; + // Best-effort like every dose write-hook: a failure is reported, never + // fails the edit. + for (const dayKey of trail.staleDoseDays) { + await refreshDoseDay({ + userId: user.id, + medicationId: id, + dayKey, + tz: user.timezone || DEFAULT_TIMEZONE, + }); + } + + // Re-read the row so the response shape carries the converged grace value. + if (primaryScheduleGracePatch) { + await prisma.medicationSchedule.update({ + where: { id: primaryScheduleGracePatch.scheduleId }, + data: { + reminderGraceMinutes: primaryScheduleGracePatch.reminderGraceMinutes, + }, + }); + const refreshed = await prisma.medication.findUnique({ + where: { id }, + include: { schedules: true }, + }); + if (refreshed) { + return refreshed; + } + } + return medication; +} + +/** + * An iPhone app build that predates custom categories (#1041) decodes an + * unknown category as OTHER and sends it back on every edit, which would + * overwrite the person's own category with OTHER. A native client (Bearer) + * sending OTHER over a custom category keeps the custom one; moving a custom + * category back to OTHER stays possible on the web. + */ +async function writeCategory( + id: string, + category: string | undefined, + authMethod: string, +) { + let categoryToWrite = category; + if (category === "OTHER" && authMethod === "bearer") { + const current = (await resolveMedicationCategories([id]))[id]; + if (current?.category.startsWith("custom:")) { + categoryToWrite = undefined; + annotate({ + action: { name: "medication.update.custom_category_kept" }, + meta: { entity_id: id }, + }); + } + } + if (categoryToWrite !== undefined) + await setMedicationCategory(id, categoryToWrite); + return (await resolveMedicationCategories([id]))[id]; +} + +/** The audit row: C4 on the medication row itself, C7 on the schedule. */ +async function auditMedicationEdit( + write: MedicationEditWrite, + medication: EditedMedication, + ipAddress: string | null, +) { + const { user, id, existing, trail } = write; + const { + replacedCadence, + replacedScheduleCount, + tombstonedSlotCount, + tombstonedFrom, + tombstonedTo, + }: EraTrail = trail; + await auditLog("medication.update", { + userId: user.id, + ipAddress, + // C4 on what the medication row itself lost, and C7 on the schedule: + // the cadence that was replaced, and how many pending slots went with + // it over which days. + details: { + medicationId: id, + ...overwriteDetails({ + before: { + name: existing.name, + dose: existing.dose, + active: existing.active, + asNeeded: existing.asNeeded, + trackIntake: existing.trackIntake, + // Switching automatic recording on or off is a change + // to how doses are recorded, so the row names it. + autoMarkTaken: existing.autoMarkTaken, + endsOn: existing.endsOn, + }, + after: { + name: medication.name, + dose: medication.dose, + active: medication.active, + asNeeded: medication.asNeeded, + trackIntake: medication.trackIntake, + autoMarkTaken: medication.autoMarkTaken, + endsOn: medication.endsOn, + }, + }), + ...(replacedScheduleCount !== null + ? { + replacedCadence, + replacedScheduleCount, + tombstonedSlotCount, + tombstonedFrom: tombstonedFrom?.toISOString() ?? null, + tombstonedTo: tombstonedTo?.toISOString() ?? null, + } + : {}), + }, + }); +} + +/** Edit one medication the route found and authorised. */ +export async function updateMedication(input: { + user: EditUser; + authMethod: string; + id: string; + existing: EditableMedication; + data: MedicationEdit; + ipAddress: string | null; +}): Promise { + const { user, id } = input; + const plan = await planMedicationEdit(input); + if (!plan.ok) return plan; + const { write } = plan; + + let medication: EditedMedication; + try { + medication = await writeMedicationEdit(write); + } catch (err) { + // A concurrent course write landed between the check above and the + // course write. The transaction rolled back: nothing was written. + if (err instanceof CourseWriteError) { + const r = courseRefusalResponse(err.refusal); + return { + ok: false, + message: r.message, + status: r.status, + meta: { errorCode: r.errorCode }, + }; + } + throw err; + } + + medication = await settleAfterCommit( + write, + medication, + plan.primaryScheduleGracePatch, + ); + + const resolvedCategory = await writeCategory( + id, + write.input.category, + input.authMethod, + ); + const normalizedCategory = resolvedCategory?.category ?? "OTHER"; + + // A changed units-per-dose (on the medication or on a slot) changes what + // every taken dose added, so a medication with a composition has its doses + // recomputed. Cheap and coalesced when nothing moved. + if (await medicationHasComposition(user.id, id)) { + await enqueueSupplementNutrientRecompute(user.id); + } + + await auditMedicationEdit(write, medication, input.ipAddress); + + annotate({ + action: { + name: "medication.update", + entity_type: "medication", + entity_id: id, + }, + }); + + // Bust per-user medications + compliance + achievement caches so the next + // read reflects the schedule change. + invalidateUserMedications(user.id, { evict: true }); + + return { + ok: true, + body: { + ...medication, + unitsPerDose: Number(medication.unitsPerDose), + ...scheduleWireFields( + medication.trackIntake, + serializeScheduleUnitsPerDose( + medication.schedules, + medication.unitsPerDose, + ), + ), + category: normalizedCategory, + categoryLabel: resolvedCategory?.categoryLabel ?? null, + // The courses, as every medication read carries them (#1024). + ...( + await resolveCourseFields([medication], new Date(), write.courseTz) + ).get(id), + }, + }; +} diff --git a/src/lib/medications/update-medication/write.ts b/src/lib/medications/update-medication/write.ts new file mode 100644 index 000000000..c15b69575 --- /dev/null +++ b/src/lib/medications/update-medication/write.ts @@ -0,0 +1,455 @@ +/** + * The write of a medication edit: everything the PUT changes on the + * medication, its schedule and its course commits as one transaction. + * + * The course write can still be refused after the preflight check (another + * request wrote a course in between); with the row update committed first, + * the client got the refusal while the name, the schedule and `oneShot` were + * already changed. Inside one transaction a refusal rolls all of it back. The + * dose-day refresh and the grace bridge stay outside: both read or write what + * this commit settles, and neither may undo it. + */ +import type { Prisma } from "@/generated/prisma/client"; +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import type { SessionUser } from "@/lib/auth/session-user"; +import { setCurrentWindow } from "@/lib/medications/courses"; +import { + schedulesMateriallyDiffer, + toRevisionPayloadEntry, +} from "@/lib/medications/scheduling/schedule-eras"; +import { moveAutoTakeFloorToNow } from "@/lib/medications/auto-take"; +import { dayKeyForScheduledFor } from "@/lib/medications/dose-day-refresh"; +import { getUserTodayBounds } from "@/lib/tz/local-day"; +import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; +import type { EditableMedication } from "./preflight"; +import type { MedicationEdit, NormalisedSchedule } from "./schedule-normalise"; + +/** + * C7. What a schedule replacement destroys, captured for the audit row: the + * cadence that was there and the pending slots that were tombstoned with it. + * A feed line saying "changed a medication" cannot answer "back to what?", + * and unlike a row a schedule keeps ACTING after the manager's grant ends. + * `staleDoseDays` holds the dose days the tombstoned slots sat on, refreshed + * once the transaction has committed the tombstones. + */ +export interface EraTrail { + replacedCadence: string | null; + replacedScheduleCount: number | null; + tombstonedSlotCount: number | null; + tombstonedFrom: Date | null; + tombstonedTo: Date | null; + staleDoseDays: Set; +} + +export function createEraTrail(): EraTrail { + return { + replacedCadence: null, + replacedScheduleCount: null, + tombstonedSlotCount: null, + tombstonedFrom: null, + tombstonedTo: null, + staleDoseDays: new Set(), + }; +} + +export interface MedicationEditWrite { + user: Pick; + id: string; + existing: EditableMedication; + input: MedicationEdit; + normalisedSchedules: NormalisedSchedule[] | undefined; + wasTracked: boolean; + willTrack: boolean; + trackingChanged: boolean; + pausedAtPatch: { pausedAt?: Date | null }; + touchesWindow: boolean; + nextWindow: { startsOn: Date | null; endsOn: Date | null }; + courseTz: string; + trail: EraTrail; +} + +type RevisionSourceRow = Parameters[0]; + +/** + * Tombstone today's and future open pending rows of this medication. Runs on + * a schedule replace (the old anchors are stale) and when intake tracking is + * switched off (nothing is due any more). + */ +async function tombstoneOpenSlots( + db: Prisma.TransactionClient, + edit: MedicationEditWrite, +): Promise { + const { user, id, trail } = edit; + // A schedule replace invalidates the open slot anchors the projector / + // reminder worker minted for the OLD times: a pending 08:00 row for a + // medication that now doses at 20:00 would linger as a phantom slot, + // auto-miss later, and depress compliance. Tombstone today's and + // future open pending rows (never an actioned row — `takenAt`, + // `skipped`, and `autoMissed` are user-visible history) and bump + // `syncVersion` so delta-sync clients drop them. The projector and + // the reminder worker re-mint the anchors for the new times on their + // next pass — the route does not pre-create them. + const userTz = user.timezone || DEFAULT_TIMEZONE; + const { start: todayStart } = getUserTodayBounds(new Date(), userTz); + const tombstoneWhere = { + userId: user.id, + medicationId: id, + deletedAt: null, + takenAt: null, + skipped: false, + autoMissed: false, + scheduledFor: { gte: todayStart }, + } as const; + // Capture the affected slots BEFORE the tombstone so the compliance + // rollups for those days can be recomputed after the commit — + // `updateMany` does not return the touched rows. Without the + // recompute a tombstoned pending kept counting as `scheduled` in + // the rollup row until the next event-driven write for that day, + // depressing the rate the cards read in the meantime. + const tombstoned = await db.medicationIntakeEvent.findMany({ + where: tombstoneWhere, + select: { scheduledFor: true }, + }); + // The count and the day range of the anchors that went. + trail.tombstonedSlotCount = tombstoned.length; + for (const row of tombstoned) { + if ( + trail.tombstonedFrom === null || + row.scheduledFor < trail.tombstonedFrom + ) { + trail.tombstonedFrom = row.scheduledFor; + } + if (trail.tombstonedTo === null || row.scheduledFor > trail.tombstonedTo) { + trail.tombstonedTo = row.scheduledFor; + } + } + await db.medicationIntakeEvent.updateMany({ + where: tombstoneWhere, + data: { deletedAt: new Date(), syncVersion: { increment: 1 } }, + }); + // Deduped via the user-tz day key. + for (const row of tombstoned) { + trail.staleDoseDays.add(dayKeyForScheduledFor(row.scheduledFor, userTz)); + } +} + +function revisionEntryOf(row: RevisionSourceRow) { + return toRevisionPayloadEntry({ + timesOfDay: row.timesOfDay, + windowStart: row.windowStart, + windowEnd: row.windowEnd, + daysOfWeek: row.daysOfWeek, + rrule: row.rrule, + rollingIntervalDays: row.rollingIntervalDays, + scheduleType: row.scheduleType, + cyclicOnWeeks: row.cyclicOnWeeks, + cyclicOffWeeks: row.cyclicOffWeeks, + doseWindows: row.doseWindows, + label: row.label, + dose: row.dose, + reminderGraceMinutes: row.reminderGraceMinutes, + }); +} + +/** One archived era covering `[previous validUntil | createdAt, now)`. */ +async function archiveEra( + tx: Prisma.TransactionClient, + edit: MedicationEditWrite, + payload: unknown[], + meta: Record, +): Promise { + const { id, existing } = edit; + const lastRevision = await tx.medicationScheduleRevision.findFirst({ + // Chain from the ACTIVE boundary: a superseded row is an + // audit record whose `validUntil` may sit past its correction. + where: { medicationId: id, supersededByRevisionId: null }, + orderBy: { validUntil: "desc" }, + select: { validUntil: true }, + }); + await tx.medicationScheduleRevision.create({ + data: { + medicationId: id, + validFrom: lastRevision?.validUntil ?? existing.createdAt, + validUntil: new Date(), + payload: payload as unknown as Prisma.InputJsonValue, + }, + }); + annotate({ + action: { + name: "medication.schedule.revision_archived", + entity_type: "medication", + entity_id: id, + }, + meta, + }); +} + +/** The cadence a schedule replace destroys, as one comparable line. */ +function cadenceOf(previousRows: RevisionSourceRow[]): string | null { + return previousRows.length > 0 + ? previousRows + .map((row) => + [ + row.timesOfDay, + row.rrule ?? "", + row.rollingIntervalDays !== null + ? `every ${row.rollingIntervalDays}d` + : "", + row.daysOfWeek ?? "", + ] + .filter((part) => part !== "") + .join(" "), + ) + .join(" | ") + : null; +} + +/** + * A schedule replace. Effective dating: before the wholesale replace wipes + * the old rows, archive them as ONE revision covering `[previous validUntil | + * medication.createdAt, now)` — but only when a cadence-relevant field + * actually changes. A no-op edit (the Zeitplan tab echoing the same rows + * back) must not mint a phantom era. + */ +async function replaceSchedules( + tx: Prisma.TransactionClient, + edit: MedicationEditWrite, + normalisedSchedules: NormalisedSchedule[], +): Promise { + const { id, existing, input, wasTracked, trackingChanged, trail } = edit; + const previousRows = await tx.medicationSchedule.findMany({ + where: { medicationId: id }, + }); + const previousEntries = previousRows.map(revisionEntryOf); + trail.replacedScheduleCount = previousRows.length; + trail.replacedCadence = cadenceOf(previousRows); + + // A stretch with intake tracking off expected nothing (#1033), so it + // archives with an empty payload whatever the rows said; and a tracking + // transition is itself an era boundary even when the rows stay the same. + const archivedEntries = wasTracked ? previousEntries : []; + if ( + previousRows.length > 0 && + (trackingChanged || + !wasTracked || + schedulesMateriallyDiffer( + previousEntries, + normalisedSchedules.map((n) => n.snapshot), + )) + ) { + await archiveEra(tx, edit, archivedEntries, { + schedule_revision_rows: archivedEntries.length, + }); + } else if ( + previousRows.length === 0 && + existing.asNeeded && + input.asNeeded === false && + normalisedSchedules.length > 0 + ) { + // Flipping as-needed OFF. The medication carried ZERO + // schedule rows while as-needed, so the wholesale-replace archive + // above never fires — and without a revision the live era would + // start at the PREVIOUS revision's `validUntil` (or `createdAt`), + // retro-painting the schedule-less as-needed stretch with the NEW + // schedule's expected slots: every PRN day would read as missed. + // Archive an EMPTY era covering the as-needed stretch instead; an + // empty payload expands to zero schedules, so era-aware compliance + // expects nothing there — exactly the as-needed contract. + await archiveEra(tx, edit, [], { schedule_revision_rows: 0 }); + } + await tombstoneOpenSlots(tx, edit); + await tx.medicationSchedule.deleteMany({ + where: { medicationId: id }, + }); +} + +/** + * Intake tracking switched without a schedule replace (#1033). Close the + * current era at this instant: switching OFF archives the schedule that was + * in force (the tracked stretch keeps its expected doses); switching back ON + * archives the untracked stretch with an empty payload, so compliance never + * counts it as missed and the live schedule, the projector and the reminder + * worker all resume from now. + */ +async function archiveTrackingSwitch( + tx: Prisma.TransactionClient, + edit: MedicationEditWrite, +): Promise { + const { id, wasTracked, willTrack } = edit; + const liveRows = await tx.medicationSchedule.findMany({ + where: { medicationId: id }, + }); + if (liveRows.length > 0) { + const archivedEntries = wasTracked ? liveRows.map(revisionEntryOf) : []; + await archiveEra(tx, edit, archivedEntries, { + schedule_revision_rows: archivedEntries.length, + track_intake: willTrack, + }); + } +} + +/** The medication row's update, field-by-field from the parsed body. */ +function medicationUpdateData(edit: MedicationEditWrite) { + const { existing, input, normalisedSchedules } = edit; + const { + name, + dose, + treatmentClass, + dosesPerUnit, + unitsPerDose, + reorderLeadDays, + deliveryForm, + trackInjectionSites, + allowedInjectionSites, + active, + notificationsEnabled, + liveActivityEnabled, + criticalAlarmEnabled, + atcCode, + rxNormCode, + oneShot, + asNeeded, + trackIntake, + autoMarkTaken, + } = input; + return { + ...(name !== undefined && { name }), + ...(dose !== undefined && { dose }), + ...(treatmentClass !== undefined && { treatmentClass }), + ...(dosesPerUnit !== undefined && { dosesPerUnit }), + // Units consumed per dose. Field-by-field; the stamp on + // already-taken intake events freezes their recorded consumption. + ...(unitsPerDose !== undefined && { unitsPerDose }), + // Reorder lead override. Field-by-field; `null` is a + // valid explicit value (clears the override → user-level default). + ...(reorderLeadDays !== undefined && { reorderLeadDays }), + ...(deliveryForm !== undefined && { deliveryForm }), + // Injection-site tracking opt-in + per-medication allowed + // sites. Field-by-field; `false` / `[]` are valid explicit values + // (deactivate tracking / clear the per-med restriction). + ...(trackInjectionSites !== undefined && { trackInjectionSites }), + ...(allowedInjectionSites !== undefined && { allowedInjectionSites }), + ...(active !== undefined && { active }), + ...(notificationsEnabled !== undefined && { notificationsEnabled }), + // iOS reminder flags, field-by-field. + ...(liveActivityEnabled !== undefined && { liveActivityEnabled }), + ...(criticalAlarmEnabled !== undefined && { criticalAlarmEnabled }), + // Optional drug-classification codes (ATC / RxNorm). + // Field-by-field; `null` is a valid explicit value (clears the code). + ...(atcCode !== undefined && { atcCode }), + ...(rxNormCode !== undefined && { rxNormCode }), + // Scheduling primitives — pass-through when supplied. + // `startsOn` / `endsOn` are not written here (#1024): the + // course writer below writes them together with the course they + // project, in one transaction, so the row and its courses cannot + // disagree if one of the two writes fails. + ...(oneShot !== undefined && { oneShot }), + // As-needed flag, field-by-field (the invariants above + // already guaranteed the medication ends schedule-consistent). + ...(asNeeded !== undefined && { asNeeded }), + // Intake tracking, field-by-field (#1033). + ...(trackIntake !== undefined && { trackIntake }), + // Auto-mark-taken, field-by-field. Only an off→on flip + // stamps `autoMarkTakenSince`, so re-saving the form with the + // option already on never moves the floor; switching it off + // clears the stamp. + ...(autoMarkTaken !== undefined && + autoMarkTaken !== existing.autoMarkTaken && { + autoMarkTaken, + autoMarkTakenSince: autoMarkTaken ? new Date() : null, + }), + ...(normalisedSchedules && { + schedules: { + create: normalisedSchedules.map((n) => n.createData), + }, + }), + }; +} + +/** + * Durable pause intervals. The `pausedAt` column is a single live marker that + * resume clears, so the paused window is irrecoverable once resumed and every + * paused day's slot collapses to "missed" in the compliance denominator. + * Mirror the live transition onto an additive era record the rate paths read: + * a pause opens an era (resumedAt null), a resume closes the latest open era. + * `userId` comes from the authenticated session; the data object is built + * field-by-field. + */ +async function recordPauseTransition( + tx: Prisma.TransactionClient, + edit: MedicationEditWrite, +): Promise { + const { user, id, existing } = edit; + const { active } = edit.input; + if (active === false && existing.active) { + await tx.medicationPauseEra.create({ + data: { + medicationId: id, + userId: user.id, + pausedAt: new Date(), + resumedAt: null, + }, + }); + } else if (active === true && !existing.active) { + await tx.medicationPauseEra.updateMany({ + where: { medicationId: id, userId: user.id, resumedAt: null }, + data: { resumedAt: new Date() }, + }); + // Made active again: the auto-take pass starts from + // now, never filling the inactive days as taken. + await moveAutoTakeFloorToNow(tx, id); + } +} + +/** + * The transaction. Throws `CourseWriteError` when a concurrent course write + * landed between the preflight check and the course write; nothing is then + * written. + */ +export async function writeMedicationEdit(edit: MedicationEditWrite) { + const { user, id, input, normalisedSchedules, trackingChanged, willTrack } = + edit; + const { schedules } = input; + return prisma.$transaction(async (tx) => { + // If schedules provided, replace all + if (schedules && normalisedSchedules) { + await replaceSchedules(tx, edit, normalisedSchedules); + } else if (trackingChanged) { + await archiveTrackingSwitch(tx, edit); + } + + // Switching intake tracking off leaves nothing due (#1033): + // the open placeholders of today and later go the way a schedule + // replace sends them (a replace in the same request already did it). + if (trackingChanged && !willTrack && !schedules) { + await tombstoneOpenSlots(tx, edit); + } + + const baseUpdateData = medicationUpdateData(edit); + + const updated = await tx.medication.update({ + where: { id }, + data: { ...baseUpdateData, ...edit.pausedAtPatch }, + include: { schedules: true }, + }); + + await recordPauseTransition(tx, edit); + + // Write the window onto the latest course and + // project it back, then serve the projected row (#1024). + if (edit.touchesWindow) { + const projected = await setCurrentWindow( + { + userId: user.id, + medicationId: id, + timeZone: edit.courseTz, + ...edit.nextWindow, + }, + tx, + ); + return { ...updated, ...projected }; + } + return updated; + }); +} diff --git a/src/lib/mood/bulk-ingest.ts b/src/lib/mood/bulk-ingest.ts new file mode 100644 index 000000000..16b3bf76a --- /dev/null +++ b/src/lib/mood/bulk-ingest.ts @@ -0,0 +1,571 @@ +/** + * The bulk mood ingest behind `POST /api/mood-entries/bulk`. + * + * The route authenticates, meters, caps and parses the batch; this module + * writes it. Per-entry UPSERT keyed by `externalId` when the entry carries + * one, else by the legacy `(userId, date, moodLoggedAt)` key, so a re-run is + * idempotent. A bad entry is `skipped` and the rest of the batch lands: an + * unstable external id, a context outside the vocabulary, dimensions out of + * range, or a rated factor outside its own scale each refuse that one row. + */ +import type { z } from "zod/v4"; + +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import { auditLog } from "@/lib/auth/audit"; +import { markSyncCheckpoint } from "@/lib/sync/checkpoint"; +import { + getScoreForMood, + moodContextSchema, + type moodLevelEnum, + type moodSourceEnum, +} from "@/lib/validations/mood"; +import { + classifyExternalId, + unstableExternalIdMeta, + UNSTABLE_EXTERNAL_ID_REASON, + type UnstableExternalIdShape, +} from "@/lib/validations/external-id"; +import { moodDateKey } from "@/lib/mood/date-key"; +import { levelAForWrite, type LevelAInput } from "@/lib/mood/level-a"; +import { persistMoodContext } from "@/lib/mood/context"; +import { encryptNote } from "@/lib/crypto/note-cipher"; +import { invalidateUserMood } from "@/lib/cache/invalidate"; +import { recomputeMoodBucketsForEntry } from "@/lib/rollups/mood-rollups"; +import { + createTagLinks, + droppedLinkKeysForWire, + NO_DROPPED_LINK_KEYS, + type DroppedLinkKeys, + type RatedFactorInput, +} from "@/lib/mood/tag-links"; + +/** One entry as the route's batch schema parsed it. */ +export interface BulkMoodEntry { + mood: z.infer; + tags?: string[]; + tagKeys?: string[]; + ratedFactors?: RatedFactorInput[]; + note?: string; + a1?: unknown; + a2?: unknown; + a3?: unknown; + a4?: unknown; + a5?: unknown; + context?: unknown; + moodLoggedAt: Date; + source: z.infer; + externalId?: string; +} + +/** + * The route's per-entry dimension parse. It stays beside the batch schema + * so the bound is pinned against the single-entry one + * (`bulk-dimension-bounds.test.ts`). + */ +export type ParseBulkDimensions = ( + dimensions: Pick, +) => { success: true; data: LevelAInput } | { success: false }; + +type EntryStatus = "inserted" | "duplicate" | "skipped"; +interface EntryResult { + index: number; + status: EntryStatus; + reason?: string; + id?: string; + // Echo the client-supplied source-stable id back on each result so iOS can + // map a server row id onto its local SwiftData row without re-deriving it. + // Omitted when the entry sent no externalId. + externalId?: string; + // Submitted keys the catalog did not resolve (unknown, archived, another + // account's custom tag, or the wrong kind). Present only when non-empty, + // on an "inserted" or "duplicate" result: the entry landed, these did not. + droppedTagKeys?: string[]; + droppedFactorKeys?: string[]; +} + +export interface BulkMoodIngestResult { + processed: number; + inserted: number; + duplicates: number; + skipped: Array<{ index: number; reason: string }>; + entries: EntryResult[]; +} + +interface ExistingMoodRow { + id: string; + deletedAt: Date | null; +} + +interface ExistingRows { + byExternalKey: Map; + byDateKey: Map; +} + +/** The running totals of one batch, filled entry by entry. */ +interface Tally { + results: EntryResult[]; + skipped: Array<{ index: number; reason: string }>; + inserted: number; + duplicates: number; + droppedTagKeyCount: number; + droppedFactorKeyCount: number; + unstableShapes: UnstableExternalIdShape[]; +} + +function echoExternalId(entry: BulkMoodEntry): { externalId?: string } { + return entry.externalId ? { externalId: entry.externalId } : {}; +} + +function skip( + tally: Tally, + index: number, + entry: BulkMoodEntry, + reason: string, +): void { + tally.skipped.push({ index, reason }); + tally.results.push({ + index, + status: "skipped", + reason, + ...echoExternalId(entry), + }); +} + +/** + * Perf — a single indexed existence read replaces the per-entry `findUnique` + * probe (up to 500 sequential round trips before). Every entry still resolves + * through exactly the key its own dedup contract uses (externalId when + * carried, else the legacy date+moodLoggedAt key), so the lookups are + * byte-identical to the per-entry probe; only the read is batched. The + * per-entry `upsert` + tombstone check + tag links stay per entry; batching + * those further would risk the correctness-sensitive per-row side effects + * for a write path that is usually small (a normal sync posts the current + + * previous local day, not a full historical backfill). + */ +async function readExistingRows( + userId: string, + tz: string, + entries: BulkMoodEntry[], +): Promise { + const orClauses: Array< + | { source: string; externalId: string } + | { date: string; moodLoggedAt: Date } + > = entries.map((entry) => + entry.externalId + ? { source: entry.source, externalId: entry.externalId } + : { + date: moodDateKey(entry.moodLoggedAt, tz), + moodLoggedAt: entry.moodLoggedAt, + }, + ); + const existingRows = + orClauses.length > 0 + ? await prisma.moodEntry.findMany({ + where: { userId, OR: orClauses }, + select: { + id: true, + deletedAt: true, + source: true, + externalId: true, + date: true, + moodLoggedAt: true, + }, + }) + : []; + const byExternalKey = new Map(); + const byDateKey = new Map(); + for (const row of existingRows) { + if (row.externalId !== null) { + byExternalKey.set(`${row.source}::${row.externalId}`, row); + } + byDateKey.set(`${row.date}::${row.moodLoggedAt.toISOString()}`, row); + } + return { byExternalKey, byDateKey }; +} + +/** + * The upsert and the tag/factor links run in ONE transaction, matching the + * single-entry POST path. Split apart, an out-of-scale `ratedFactors` value + * (validated inside `createTagLinks`, i.e. AFTER the row committed) left a + * persisted mood entry with no links while the per-entry catch reported the + * entry as "skipped": the client believes nothing saved and a half-written + * row is on disk. + */ +async function writeEntry(args: { + userId: string; + tz: string; + entry: BulkMoodEntry; + date: string; + levelA: ReturnType; + context: z.infer | null | undefined; +}): Promise<{ id: string; droppedKeys: DroppedLinkKeys }> { + const { userId, tz, entry, date, levelA } = args; + const score = getScoreForMood(entry.mood); + // When the entry carries a source-stable `externalId`, dedup on the + // NULL-distinct `(userId, source, externalId)` key so a re-post with the + // same id (an iOS retry after a network hiccup, or a second adopt-on-pair + // backfill) updates the existing row in place instead of minting a + // duplicate when `moodLoggedAt` re-rounds / re-zones. Absent → the legacy + // `(userId, date, moodLoggedAt)` key. `source` carries a schema default of + // "MANUAL"; resolve it once so the probe, upsert key, and create write all + // agree on the value. + const resolvedSource = entry.source; + const probeWhere = entry.externalId + ? { + userId_source_externalId: { + userId, + source: resolvedSource, + externalId: entry.externalId, + }, + } + : { + userId_date_moodLoggedAt: { + userId, + date, + moodLoggedAt: entry.moodLoggedAt, + }, + }; + return prisma.$transaction(async (tx) => { + const upserted = await tx.moodEntry.upsert({ + where: probeWhere, + create: { + userId, + date, + tz, + mood: entry.mood, + score, + ...levelA, + tags: entry.tags ? JSON.stringify(entry.tags) : null, + note: null, + noteEncrypted: encryptNote(entry.note ?? null), + source: resolvedSource, + externalId: entry.externalId ?? null, + moodLoggedAt: entry.moodLoggedAt, + }, + update: { + // Last-writer-wins on the mood + tags + note triple. The iOS client + // only re-posts an existing entry when it has new data; the server + // trusts that decision. When the dedup key is `externalId`, also + // refresh `date` / `moodLoggedAt` so a re-zoned re-import lands the + // corrected wall-clock on the same row. + mood: entry.mood, + score, + ...levelA, + tags: entry.tags ? JSON.stringify(entry.tags) : null, + note: null, + noteEncrypted: encryptNote(entry.note ?? null), + ...(entry.externalId ? { date, moodLoggedAt: entry.moodLoggedAt } : {}), + }, + }); + + // Structured-tag links, mirroring the single-entry `createTagLinks` + // path. Additive + idempotent: `createTagLinks` resolves keys against the + // catalog (dropping unknown keys) and `skipDuplicates` on the join insert + // keeps a re-posted entry from minting duplicate links. Runs for both + // fresh and re-posted (upserted) rows so a backfill that adds tag keys on + // a second pass still lands them. Rated factors ride the same path; an + // out-of-scale rating throws `RatedFactorOutOfRangeError`, which inside + // the transaction rolls the mood row back with it, so a "skipped" result + // means nothing was written. The day context sits inside the same + // transaction as the row for the same reason. + await persistMoodContext(tx, upserted.id, userId, args.context); + + // The keys the catalog did not resolve are reported on this entry's + // result: "inserted" alone told a client its tags had landed. + let droppedKeys = NO_DROPPED_LINK_KEYS; + if ( + (entry.tagKeys && entry.tagKeys.length > 0) || + (entry.ratedFactors && entry.ratedFactors.length > 0) + ) { + droppedKeys = await createTagLinks( + upserted.id, + userId, + entry.tagKeys ?? [], + tx, + entry.ratedFactors ?? [], + ); + } + return { id: upserted.id, droppedKeys }; + }); +} + +/** + * P2002 = a concurrent batch raced the same NULL-distinct + * `(userId, source, externalId)` key and won. That is the plain duplicate the + * sequential probe would have classified; reporting it as "skipped" with a + * raw Prisma message told the client its row was dropped when it is in fact + * stored. Resolve the winning row's id so the caller's cursor advances, + * exactly like the intake bulk route. + */ +function isRacedDuplicate(err: unknown): boolean { + return ( + typeof err === "object" && + err !== null && + "code" in err && + (err as { code: string }).code === "P2002" + ); +} + +async function ingestEntry(args: { + userId: string; + tz: string; + entry: BulkMoodEntry; + index: number; + existing: ExistingRows; + parseDimensions: ParseBulkDimensions; + tally: Tally; +}): Promise { + const { userId, tz, entry, index: i, tally } = args; + + // The `(userId, source, externalId)` dedup key only holds while the id is + // STABLE across client launches; a per-process value (an object + // description carrying a memory address) mints a fresh entry on every + // re-post. Per-entry refusal, never a whole-batch 422. + const unstable = + entry.externalId === undefined + ? null + : classifyExternalId(entry.externalId); + if (unstable) { + tally.unstableShapes.push(unstable); + tally.skipped.push({ index: i, reason: UNSTABLE_EXTERNAL_ID_REASON }); + tally.results.push({ + index: i, + status: "skipped", + reason: UNSTABLE_EXTERNAL_ID_REASON, + externalId: entry.externalId, + }); + return; + } + + // The context, parsed for THIS entry. A value outside the vocabulary skips + // this row and nothing else. + const parsedContext = + entry.context === undefined + ? { success: true as const, data: undefined } + : moodContextSchema.nullable().safeParse(entry.context); + if (!parsedContext.success) { + skip(tally, i, entry, "invalid_context"); + return; + } + + // The five dimensions, parsed for THIS entry. Out of range skips this row + // and nothing else. + const parsedDimensions = args.parseDimensions({ + a1: entry.a1, + a2: entry.a2, + a3: entry.a3, + a4: entry.a4, + a5: entry.a5, + }); + if (!parsedDimensions.success) { + skip(tally, i, entry, "invalid_dimensions"); + return; + } + + const date = moodDateKey(entry.moodLoggedAt, tz); + // The same resolution the single-entry path uses: pleasantness derived + // from the label unless the entry states its own, and the other four + // present only when this entry carried them. An omitted dimension is left + // alone rather than written as null, so a row that already carries + // hand-set values keeps them across a re-import. + const levelA = levelAForWrite(entry.mood, parsedDimensions.data); + + try { + // The response distinguishes "inserted" from "duplicate" via the batched + // pre-read (keyed exactly like the upsert key), so the per-entry probe + // here is a Map lookup, not a round trip. + const existing = + (entry.externalId + ? args.existing.byExternalKey.get( + `${entry.source}::${entry.externalId}`, + ) + : args.existing.byDateKey.get( + `${date}::${entry.moodLoggedAt.toISOString()}`, + )) ?? null; + + // Tombstone suppression: a soft-deleted match stays deleted. The + // user-facing DELETE route flips `deletedAt` so the `/api/sync/changes` + // feed surfaces the deletion to paired clients offline at delete time + // (see the MoodEntry schema note); an offline client's later re-post of + // the same entry is stale state, not a recreation, and must not resurrect + // the row. True no-op: no value churn, no `updatedAt` bump, no tag-link + // writes on the hidden row; report `duplicate` so the client checkpoints + // past it exactly like a live-row match. + if (existing?.deletedAt) { + tally.duplicates += 1; + tally.results.push({ + index: i, + status: "duplicate", + id: existing.id, + ...echoExternalId(entry), + }); + return; + } + + const written = await writeEntry({ + userId, + tz, + entry, + date, + levelA, + context: parsedContext.data, + }); + tally.droppedTagKeyCount += written.droppedKeys.droppedTagKeys.length; + tally.droppedFactorKeyCount += written.droppedKeys.droppedFactorKeys.length; + + if (existing) tally.duplicates += 1; + else tally.inserted += 1; + tally.results.push({ + index: i, + status: existing ? "duplicate" : "inserted", + id: written.id, + ...echoExternalId(entry), + ...droppedLinkKeysForWire(written.droppedKeys), + }); + } catch (err: unknown) { + if (isRacedDuplicate(err) && entry.externalId) { + const winner = await prisma.moodEntry.findFirst({ + where: { + userId, + // Same resolution as the upsert key: the schema defaults `source` + // to "MANUAL" at parse time. + source: entry.source, + externalId: entry.externalId, + }, + select: { id: true }, + }); + tally.duplicates += 1; + tally.results.push({ + index: i, + status: "duplicate", + ...(winner ? { id: winner.id } : {}), + externalId: entry.externalId, + }); + return; + } + const reason = + err instanceof Error ? err.message.slice(0, 120) : "upsert_failed"; + skip(tally, i, entry, reason); + } +} + +/** + * Refresh the rollup tier for every distinct day the batch touched. The bulk + * endpoint is an iOS one-shot backfill, so the batch can span many days; the + * set of `date` labels (the same per-row `moodDateKey` the insert used, so + * the rollup keys byte-identically to the stored `MoodEntry.date`) bounds the + * recompute count. Best-effort: a rollup failure must not surface as a 5xx. + */ +async function refreshTouchedRollups( + userId: string, + tz: string, + entries: BulkMoodEntry[], + results: EntryResult[], +): Promise { + const touchedLabels = new Set(); + for (let i = 0; i < entries.length; i++) { + const status = results[i]?.status; + if (status === "inserted" || status === "duplicate") { + touchedLabels.add(moodDateKey(entries[i].moodLoggedAt, tz)); + } + } + try { + await Promise.all( + Array.from(touchedLabels).map((label) => + recomputeMoodBucketsForEntry(userId, label), + ), + ); + } catch (rollupErr) { + annotate({ + meta: { + mood_rollup_write_failed: true, + mood_rollup_write_error: + rollupErr instanceof Error ? rollupErr.message : String(rollupErr), + }, + }); + } +} + +/** Write one parsed batch for `userId` and answer its per-entry envelope. */ +export async function ingestMoodBulk(input: { + userId: string; + tz: string; + entries: BulkMoodEntry[]; + ipAddress: string | null; + parseDimensions: ParseBulkDimensions; +}): Promise { + const { userId, tz, entries } = input; + const tally: Tally = { + results: [], + skipped: [], + inserted: 0, + duplicates: 0, + droppedTagKeyCount: 0, + droppedFactorKeyCount: 0, + unstableShapes: [], + }; + const existing = await readExistingRows(userId, tz, entries); + + for (let i = 0; i < entries.length; i++) { + await ingestEntry({ + userId, + tz, + entry: entries[i], + index: i, + existing, + parseDimensions: input.parseDimensions, + tally, + }); + } + + await auditLog("mood.bulk.ingest", { + userId, + ipAddress: input.ipAddress, + details: { + processed: entries.length, + inserted: tally.inserted, + duplicates: tally.duplicates, + skipped: tally.skipped.length, + }, + }); + + annotate({ + action: { name: "mood.bulk.ingest" }, + meta: { + processed: entries.length, + inserted: tally.inserted, + duplicates: tally.duplicates, + skipped: tally.skipped.length, + dropped_tag_keys: tally.droppedTagKeyCount, + dropped_factor_keys: tally.droppedFactorKeyCount, + ...(tally.unstableShapes.length > 0 + ? unstableExternalIdMeta("mood.bulk", tally.unstableShapes) + : {}), + }, + }); + + // Bust the per-user mood + achievements + analytics caches when at least + // one row landed so the next read picks up the ingested batch. Skipped / + // duplicate-only ingests are no-ops. + if (tally.inserted > 0) { + invalidateUserMood(userId); + } + + if (tally.inserted > 0 || tally.duplicates > 0) { + await refreshTouchedRollups(userId, tz, entries, tally.results); + } + + // The SyncMode checkpoint belongs to the push that moved the data, not to + // the `/api/sync/state` handshake that reports it. This endpoint is one of + // the three domains that handshake summarises, so a mood drain advances it. + await markSyncCheckpoint(userId); + + return { + processed: entries.length, + inserted: tally.inserted, + duplicates: tally.duplicates, + skipped: tally.skipped, + entries: tally.results, + }; +} diff --git a/src/lib/openapi/routes/cycle.ts b/src/lib/openapi/routes/cycle.ts index 1fdbdb077..b16c95ff9 100644 --- a/src/lib/openapi/routes/cycle.ts +++ b/src/lib/openapi/routes/cycle.ts @@ -43,10 +43,10 @@ import { import { isoDateTime } from "@/lib/validations/datetime"; // ── Cycle tracking (v1.15.0) ───────────────────────────────────────── -// The `/api/cycle/*` capture / calendar / history / settings surface + +// The `/api/cycle` capture / calendar / history / settings surface + // the cycle-prefs PATCH. Request bodies come from the Zod validation // module so the spec stays single-source; response DTOs are declared -// here mirroring `src/lib/cycle/dto.ts`. Every `/api/cycle/*` route also +// here mirroring `src/lib/cycle/dto.ts`. Every `/api/cycle` route also // 403s `{ errorCode:"cycle.disabled" }` when the feature gate is off. const predictionMethodEnumOpenapi = z diff --git a/src/lib/openapi/routes/documents.ts b/src/lib/openapi/routes/documents.ts index 0808f1686..182a0cee2 100644 --- a/src/lib/openapi/routes/documents.ts +++ b/src/lib/openapi/routes/documents.ts @@ -379,11 +379,31 @@ const documentUploadReceipt = z }); // Shared by the POST 200 (duplicate) and 201 (created) responses — the -// envelope id must be registered exactly once. -const documentUploadEnvelope = dataEnvelope( - z.union([inboundDocument, documentUploadReceipt]), - "DocumentUploadEnvelope", -); +// envelope id must be registered exactly once. The duplicate answer flags +// itself at the envelope level, beside the request id. +const documentUploadEnvelope = z + .object({ + data: z.union([inboundDocument, documentUploadReceipt]), + error: z.null(), + meta: z + .object({ + requestId: z.string().optional(), + duplicate: z + .literal(true) + .optional() + .describe( + "Present on a 200: the bytes or the source key were already stored, and `data` is the existing row (or the receipt).", + ), + deleted: z + .literal(true) + .optional() + .describe( + "Present with `duplicate` when the source key belongs to a document the owner deleted. Nothing was stored.", + ), + }) + .optional(), + }) + .meta({ id: "DocumentUploadEnvelope" }); /** * A `DOCUMENT_EXTRACT` run's result. Smaller than the synchronous extract @@ -672,6 +692,11 @@ export const inboundDocumentPaths: NonNullable = { 'Unsupported or unidentifiable type (`meta.reason = "unsupportedType"`). Executables, HTML/SVG, and generic archives are always refused.', content: { "application/json": { schema: errorEnvelope } }, }, + "404": { + description: + "An `episodeIds` or `encounterIds` id is not a live episode or visit of the caller (`documents.inbound.episodeNotFound` / `documents.inbound.encounterNotFound`). Nothing was stored.", + content: { "application/json": { schema: errorEnvelope } }, + }, ...stdResponses, }, }, diff --git a/src/lib/openapi/routes/insights/paths.ts b/src/lib/openapi/routes/insights/paths.ts index 82d0c7e3c..d66169878 100644 --- a/src/lib/openapi/routes/insights/paths.ts +++ b/src/lib/openapi/routes/insights/paths.ts @@ -276,6 +276,15 @@ export const insightsPaths: NonNullable = { "Every provider in the chain failed for a reason that is not a rate limit — auth, timeout or an upstream 5xx. A dated failure marker is written, so the read GET reports the briefing as held rather than fresh. Also: the `briefing` capability could not be resolved (`meta.errorCode` = `ai.unavailable`, `meta.reason` = `check_failed`); nothing was generated.", content: { "application/json": { schema: errorEnvelope } }, }, + "413": { + description: "The request body is over 16 KB. Nothing was generated.", + content: { "application/json": { schema: errorEnvelope } }, + }, + "502": { + description: + "The generated briefing did not pass the outbound safety screen and was discarded (`meta.errorCode` = `insights.generate.outboundScreened`). Regenerating may succeed.", + content: { "application/json": { schema: errorEnvelope } }, + }, "401": stdResponses["401"], }, }, diff --git a/src/lib/openapi/routes/shared.ts b/src/lib/openapi/routes/shared.ts index 8dfe8b32b..4502ddc7b 100644 --- a/src/lib/openapi/routes/shared.ts +++ b/src/lib/openapi/routes/shared.ts @@ -176,7 +176,7 @@ export function listEnvelope(payload: T, id: string) { // annotated clone is exported from here under the name the use sites already // spell and the base import is aliased. A module that wants the published // form imports it from `./shared`; importing the raw schema from -// `@/lib/validations/*` instead is what inlined these anonymously. +// a `@/lib/validations` module instead is what inlined these anonymously. export const measurementTypeEnum = measurementTypeEnumBase.meta({ id: "MeasurementType", diff --git a/src/lib/openapi/routes/workouts.ts b/src/lib/openapi/routes/workouts.ts index 1fec9ab70..9483839fc 100644 --- a/src/lib/openapi/routes/workouts.ts +++ b/src/lib/openapi/routes/workouts.ts @@ -185,7 +185,21 @@ const workoutListResponse = z const workoutRouteGeometry = z .object({ geometry: z.unknown(), - sampleTimestamps: z.array(z.string()).nullable(), + sampleTimestamps: z + .array( + z.union([ + isoDateTime(), + z.object({ + t: isoDateTime(), + speedMs: z.number().optional(), + hr: z.number().int().optional(), + }), + ]), + ) + .nullable() + .describe( + "One entry per geometry point, as the track was stored: a bare instant, or (from the workout batch and the Apple Health export) an object with its instant and, where recorded, speed (m/s) and heart rate (bpm).", + ), }) .meta({ id: "WorkoutRouteGeometry" }); diff --git a/src/lib/record-activity/__tests__/verb-map-covers-routes.test.ts b/src/lib/record-activity/__tests__/verb-map-covers-routes.test.ts index 40641fa46..4bac6a6ea 100644 --- a/src/lib/record-activity/__tests__/verb-map-covers-routes.test.ts +++ b/src/lib/record-activity/__tests__/verb-map-covers-routes.test.ts @@ -244,12 +244,68 @@ describe("the matcher sees what it looks for", () => { }); }); +/** + * Delegable write routes whose handler bodies live in service modules under + * `src/lib/`, keyed by the route relative to `src/`. The route keeps the + * resolver call, so membership is read off the route; the awaited audit + * actions are read off the route AND these modules, because that is where the + * rows are written now. Each module has to be imported by its route (leg + * below), so an entry cannot outlive the delegation it describes. + */ +const HANDLER_BODY_MODULES: Record = { + "app/api/medications/route.ts": ["lib/medications/create-medication.ts"], + "app/api/medications/[id]/intake/route.ts": [ + "lib/medications/record-intake/record-intake.ts", + ], + "app/api/medications/intake/route.ts": [ + "lib/medications/update-intake-status/admission.ts", + "lib/medications/update-intake-status/update-intake-status.ts", + ], +}; + +function handlerAuditActions(file: string): string[] { + const rel = relative(join(process.cwd(), "src"), file); + return (HANDLER_BODY_MODULES[rel] ?? []).flatMap((body) => { + const path = join(process.cwd(), "src", body); + return moduleFacts(readFileSync(path, "utf8"), path).awaitedAuditActions; + }); +} + describe("the owner's activity view names what a delegate did", () => { const routes = routeFiles(API); const writeModules = routes - .map((file) => ({ file, ...moduleFacts(readFileSync(file, "utf8"), file) })) + .map((file) => { + const facts = moduleFacts(readFileSync(file, "utf8"), file); + return { + file, + writesToRecord: facts.writesToRecord, + awaitedAuditActions: [ + ...facts.awaitedAuditActions, + ...handlerAuditActions(file), + ], + }; + }) .filter((m) => m.writesToRecord); + it("reads each moved handler body where its route imports it from", () => { + const entries = Object.entries(HANDLER_BODY_MODULES); + expect(entries.length).toBeGreaterThan(0); + for (const [route, modules] of entries) { + const source = readFileSync(join(process.cwd(), "src", route), "utf8"); + expect(moduleFacts(source, route).writesToRecord, route).toBe(true); + for (const body of modules) { + const specifier = `@/${body.replace(/\.ts$/, "")}`; + expect(source, `${route} does not import ${specifier}`).toContain( + `"${specifier}"`, + ); + expect( + handlerAuditActions(join(process.cwd(), "src", route)).length, + `${body} awaits no audit row`, + ).toBeGreaterThan(0); + } + } + }); + it("reads a plausible number of route modules", () => { // A walk that finds nothing agrees with a map that covers nothing. expect(routes.length).toBeGreaterThan(300); diff --git a/src/lib/sync/changes/domain-pages.ts b/src/lib/sync/changes/domain-pages.ts new file mode 100644 index 000000000..a2100337c --- /dev/null +++ b/src/lib/sync/changes/domain-pages.ts @@ -0,0 +1,229 @@ +/** + * Turn each domain's rows into its wire block: tombstones for the + * soft-deleted rows, upserts for the live ones. + */ +import { prisma } from "@/lib/db"; +import { + findCompactionTombstones, + isFoldedWindowCandidate, + loadFoldTimezone, +} from "@/lib/measurements/folded-window"; +import { readNote } from "@/lib/crypto/note-cipher"; +import { levelAForWire } from "@/lib/mood/level-a"; +import { contextForWire } from "@/lib/mood/context"; +import { + toCycleDayLogDTO, + toMenstrualCycleDTO, + type CycleDayLogDTO, + type MenstrualCycleDTO, +} from "@/lib/cycle/dto"; + +import type { + CycleDayRow, + CycleRow, + IntakeRow, + MeasurementRow, + MoodRow, +} from "./read-rows"; +import type { + CycleDayTombstone, + DomainBlock, + IdTombstone, + IntakeUpsert, + MeasurementTombstone, + MeasurementUpsert, + MoodUpsert, +} from "./wire"; + +/** A domain's page: the rows inside the limit, and whether more follow. */ +export function pageOf( + rows: R[], + limit: number, +): { page: R[]; hasMore: boolean } { + const hasMore = rows.length > limit; + return { page: hasMore ? rows.slice(0, limit) : rows, hasMore }; +} + +function idTombstone(row: { + id: string; + syncVersion: number; + deletedAt: Date; + updatedAt: Date; +}): IdTombstone { + return { + id: row.id, + syncVersion: row.syncVersion, + deletedAt: row.deletedAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + }; +} + +/** + * Compaction tombstones are not deletions anyone made. The folds used to + * soft-delete the raw Apple Health samples they folded into an hourly or + * daily `stats:` row, and those rows rode this feed as tombstones for 75 + * days. They are left out, by the same classifier the backlog purge uses + * (`folded-window.ts`); the cursor still advances past them, so the keyset + * walk is unchanged. A person's own deletions are reported exactly as + * before. + */ +async function compactionIds( + userId: string, + page: MeasurementRow[], +): Promise> { + const compaction = new Set(); + const tombstonesOnPage = page.filter((row) => row.deletedAt !== null); + if (tombstonesOnPage.length === 0) return compaction; + const candidates = tombstonesOnPage.map((row) => ({ + type: row.type, + source: row.source, + externalId: row.externalId, + measuredAt: row.measuredAt, + deletedAt: row.deletedAt, + })); + if (candidates.some((row) => isFoldedWindowCandidate(row, row.deletedAt!))) { + const classA = await findCompactionTombstones( + prisma, + userId, + await loadFoldTimezone(prisma, userId), + candidates, + ); + for (const index of classA) compaction.add(tombstonesOnPage[index].id); + } + return compaction; +} + +export async function measurementBlock( + userId: string, + page: MeasurementRow[], +): Promise> { + const compaction = await compactionIds(userId, page); + const upserts: MeasurementUpsert[] = []; + const tombstones: MeasurementTombstone[] = []; + for (const row of page) { + if (compaction.has(row.id)) continue; + if (row.deletedAt) { + tombstones.push({ + id: row.id, + externalId: row.externalId, + syncVersion: row.syncVersion, + deletedAt: row.deletedAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + }); + } else { + // Every row carries its `source`. RECOVERY_SCORE is written by BOTH the + // WHOOP-native sync (`source = WHOOP`) and the COMPUTED proxy job + // (`source = COMPUTED`) for the same day; the client must resolve to ONE + // canonical recovery series (WHOOP wins when present, COMPUTED is the + // fallback) and never chart the proxy and the native value as two + // competing series. The feed faithfully mirrors both rows (a delta-sync + // mirror cannot drop a row mid-keyset without a tombstone); the + // canonical pick is the same WHOOP-over-COMPUTED rule the server's + // wellness tile + doctor PDF apply (`resolveCanonicalRecovery`). + upserts.push({ + id: row.id, + externalId: row.externalId, + type: row.type, + value: row.value, + unit: row.unit, + measuredAt: row.measuredAt.toISOString(), + source: row.source, + notes: readNote(row.notesEncrypted, row.notes), + syncVersion: row.syncVersion, + updatedAt: row.updatedAt.toISOString(), + }); + } + } + return { upserts, tombstones }; +} + +export function moodBlock( + page: MoodRow[], +): DomainBlock { + const upserts: MoodUpsert[] = []; + const tombstones: IdTombstone[] = []; + for (const row of page) { + if (row.deletedAt) { + tombstones.push(idTombstone({ ...row, deletedAt: row.deletedAt })); + } else { + upserts.push({ + id: row.id, + date: row.date, + mood: row.mood, + score: row.score, + // Under the same wire keys the write path accepts, so a client reads + // back exactly what it can post. + ...levelAForWire(row), + context: row.context ? contextForWire(row.context) : null, + tags: row.tags, + note: readNote(row.noteEncrypted, row.note), + moodLoggedAt: row.moodLoggedAt.toISOString(), + source: row.source, + syncVersion: row.syncVersion, + updatedAt: row.updatedAt.toISOString(), + }); + } + } + return { upserts, tombstones }; +} + +export function intakeBlock( + page: IntakeRow[], +): DomainBlock { + const upserts: IntakeUpsert[] = []; + const tombstones: IdTombstone[] = []; + for (const row of page) { + if (row.deletedAt) { + tombstones.push(idTombstone({ ...row, deletedAt: row.deletedAt })); + } else { + upserts.push({ + id: row.id, + medicationId: row.medicationId, + scheduledFor: row.scheduledFor.toISOString(), + takenAt: row.takenAt?.toISOString() ?? null, + skipped: row.skipped, + source: row.source, + autoRecorded: row.autoRecorded, + syncVersion: row.syncVersion, + updatedAt: row.updatedAt.toISOString(), + }); + } + } + return { upserts, tombstones }; +} + +export function cycleDayBlock( + page: CycleDayRow[], +): DomainBlock { + const upserts: CycleDayLogDTO[] = []; + const tombstones: CycleDayTombstone[] = []; + for (const row of page) { + if (row.deletedAt) { + tombstones.push({ + id: row.id, + externalId: row.externalId, + syncVersion: row.syncVersion, + deletedAt: row.deletedAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + }); + } else { + upserts.push(toCycleDayLogDTO(row)); + } + } + return { upserts, tombstones }; +} + +export function cycleBlock( + page: CycleRow[], +): DomainBlock { + const upserts: MenstrualCycleDTO[] = []; + const tombstones: IdTombstone[] = []; + for (const row of page) { + if (row.deletedAt) { + tombstones.push(idTombstone({ ...row, deletedAt: row.deletedAt })); + } else { + upserts.push(toMenstrualCycleDTO(row)); + } + } + return { upserts, tombstones }; +} diff --git a/src/lib/sync/changes/pull.ts b/src/lib/sync/changes/pull.ts new file mode 100644 index 000000000..594ec3a3d --- /dev/null +++ b/src/lib/sync/changes/pull.ts @@ -0,0 +1,218 @@ +/** + * One pull of the multi-domain delta feed behind `GET /api/sync/changes`. + * + * The route authenticates, meters and parses the query; this module decides + * whether the cursor can still be caught up, reads one page of every domain + * past its watermark, and answers the blocks and the next cursor. The feed + * contract is described on the route. + */ +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import { TOMBSTONE_RETENTION_DAYS } from "@/lib/auth/native-client"; +import { + decodeCursor, + decodeCursorIssuedAt, + encodeCursor, + SYNC_DOMAINS, + type SyncCursor, + type SyncDomain, +} from "@/lib/sync/cursor"; +import { isModuleEnabled } from "@/lib/modules/gate"; + +import { readDomainRows } from "./read-rows"; +import { + cycleBlock, + cycleDayBlock, + intakeBlock, + measurementBlock, + moodBlock, + pageOf, +} from "./domain-pages"; +import type { SyncChangesBody } from "./wire"; + +const DAY_MS = 86_400_000; + +/** + * Advance a domain's watermark to the last row of its page. A domain that + * returned no rows keeps its prior watermark untouched so a future pull does + * not re-scan from the start. + */ +function advanceCursor( + cursor: SyncCursor, + domain: SyncDomain, + page: Array<{ updatedAt: Date; id: string }>, +): void { + const last = page[page.length - 1]; + if (last) { + cursor[domain] = { updatedAtMs: last.updatedAt.getTime(), id: last.id }; + } +} + +/** + * Whether the cursor can no longer be caught up incrementally, and why. + * + * Past retention: ANY domain watermark older than the tombstone-retention + * horizon. Tombstones older than that may have been pruned by the cleanup + * job, so an incremental delta could silently miss a deletion. + * + * Before a reset: a cursor issued before the account's record was last + * replaced (a backup restore, or the person deleting all their data) cannot + * be caught up either: the restore wrote rows back with their original + * `updatedAt`, behind the cursor's watermarks, and removed rows without + * tombstones. Compared by the time the cursor was issued, not by its + * watermarks, which a restore leaves meaningless; a cursor from before the + * issue time was recorded counts as issued before any reset. + */ +async function expiry( + userId: string, + token: string | undefined, + cursor: SyncCursor, + serverNow: Date, +): Promise<"retention" | "sync_reset" | null> { + const retentionHorizon = new Date( + serverNow.getTime() - TOMBSTONE_RETENTION_DAYS * DAY_MS, + ); + const pastRetention = SYNC_DOMAINS.some((domain) => { + const wm = cursor[domain]; + return wm !== undefined && wm.updatedAtMs < retentionHorizon.getTime(); + }); + let beforeReset = false; + if (token) { + const account = await prisma.user.findUnique({ + where: { id: userId }, + select: { syncResetAt: true }, + }); + if (account?.syncResetAt) { + const issuedAt = decodeCursorIssuedAt(token); + beforeReset = + issuedAt === null || issuedAt < account.syncResetAt.getTime(); + } + } + if (!pastRetention && !beforeReset) return null; + return beforeReset ? "sync_reset" : "retention"; +} + +function emptyChanges(): SyncChangesBody["changes"] { + return { + measurements: { upserts: [], tombstones: [] }, + mood: { upserts: [], tombstones: [] }, + intakes: { upserts: [], tombstones: [] }, + cycleDays: { upserts: [], tombstones: [] }, + cycles: { upserts: [], tombstones: [] }, + }; +} + +export async function pullSyncChanges(input: { + userId: string; + /** The opaque cursor the client echoed, if any. */ + token: string | undefined; + limit: number; + serverNow: Date; +}): Promise { + const { userId, token, limit, serverNow } = input; + + // Module gate: cycle. The `cycleDays` / `cycles` domains carry period + // days and per-day symptom rows, the same data every `/api/cycle` route + // refuses with a 403 when the module is off. Without this the delta feed + // re-served them ungated to the very same token. + // + // OMIT, NOT 403, deliberately, and unlike the whole-record FHIR export. + // This feed multiplexes five domains over one cursor: refusing the whole + // request would wedge measurement / mood / intake catch-up for every + // account with cycle tracking off, which (the gate delegates to + // `isCycleEnabled`) is the default for most accounts. A partial payload is + // the only outcome that keeps the client syncing while still honouring the + // promise that a disabled module hands out nothing. + // + // The two cycle watermarks are deliberately NOT advanced while the module + // is off (the queries are skipped, so `advanceCursor` sees an empty page + // and leaves the prior watermark untouched). Re-enabling therefore resumes + // from where the client actually left off instead of silently skipping the + // rows that changed while the module was hidden. + // + // KNOWN GAP, out of scope here: a client that synced cycle rows BEFORE the + // module was turned off keeps its stale local mirror. Emitting tombstones + // for them would be wrong (a tombstone means deleted, and disabling a + // module must leave the rows intact so re-enabling finds a complete + // history). Purging a paired client's mirror needs an explicit "module went + // dark" signal in the feed contract plus client support for it; that is an + // additive wire change and an iOS-client change, not a server-side gate + // fix, so it is left for a coordinated contract cycle. + const cycleEnabled = await isModuleEnabled(userId, "cycle"); + + // A garbage / unparseable / old-format cursor decodes to null → fresh + // initial sync (every domain scans from the start). + const cursor: SyncCursor = token ? (decodeCursor(token) ?? {}) : {}; + + const expired = await expiry(userId, token, cursor, serverNow); + if (expired) { + annotate({ + action: { name: "sync.changes.pull" }, + meta: { + cursor_expired: true, + cursor_expired_reason: expired, + returned: 0, + }, + }); + return { + serverNow: serverNow.toISOString(), + cursor: token ?? null, + hasMore: false, + cursorExpired: true, + changes: emptyChanges(), + }; + } + + const rows = await readDomainRows(userId, cursor, limit, cycleEnabled); + const nextCursor: SyncCursor = { ...cursor }; + + const m = pageOf(rows.measurements, limit); + const measurements = await measurementBlock(userId, m.page); + advanceCursor(nextCursor, "measurements", m.page); + + const md = pageOf(rows.mood, limit); + const mood = moodBlock(md.page); + advanceCursor(nextCursor, "mood", md.page); + + const ik = pageOf(rows.intakes, limit); + const intakes = intakeBlock(ik.page); + advanceCursor(nextCursor, "intakes", ik.page); + + const cd = pageOf(rows.cycleDays, limit); + const cycleDays = cycleDayBlock(cd.page); + advanceCursor(nextCursor, "cycleDays", cd.page); + + const cy = pageOf(rows.cycles, limit); + const cycles = cycleBlock(cy.page); + advanceCursor(nextCursor, "cycles", cy.page); + + const hasMore = + m.hasMore || md.hasMore || ik.hasMore || cd.hasMore || cy.hasMore; + + annotate({ + action: { name: "sync.changes.pull" }, + meta: { + measurement_upserts: measurements.upserts.length, + measurement_tombstones: measurements.tombstones.length, + mood_upserts: mood.upserts.length, + mood_tombstones: mood.tombstones.length, + intake_upserts: intakes.upserts.length, + intake_tombstones: intakes.tombstones.length, + cycle_day_upserts: cycleDays.upserts.length, + cycle_day_tombstones: cycleDays.tombstones.length, + cycle_upserts: cycles.upserts.length, + cycle_tombstones: cycles.tombstones.length, + cycle_module_enabled: cycleEnabled, + has_more: hasMore, + cursor_present: Boolean(token), + }, + }); + + return { + serverNow: serverNow.toISOString(), + cursor: encodeCursor(nextCursor, serverNow.getTime()), + hasMore, + cursorExpired: false, + changes: { measurements, mood, intakes, cycleDays, cycles }, + }; +} diff --git a/src/lib/sync/changes/read-rows.ts b/src/lib/sync/changes/read-rows.ts new file mode 100644 index 000000000..9beae88f4 --- /dev/null +++ b/src/lib/sync/changes/read-rows.ts @@ -0,0 +1,147 @@ +/** + * The per-domain keyset walks of the delta feed. + * + * Live and soft-deleted rows come out of the same scan: a soft-delete bumps + * `updatedAt`, so a tombstone is just a row whose `deletedAt` is non-null. + * Each walk fetches `limit + 1` rows to detect `hasMore` without a count + * query. + */ +import { prisma } from "@/lib/db"; +import type { Prisma } from "@/generated/prisma/client"; +import type { DomainWatermark, SyncCursor } from "@/lib/sync/cursor"; +import { dayLogSymptomInclude } from "@/lib/cycle/dto"; + +const MEASUREMENT_SELECT = { + id: true, + externalId: true, + type: true, + value: true, + unit: true, + measuredAt: true, + source: true, + notes: true, + notesEncrypted: true, + syncVersion: true, + deletedAt: true, + updatedAt: true, +} satisfies Prisma.MeasurementSelect; + +const MOOD_SELECT = { + id: true, + date: true, + mood: true, + score: true, + moodA1: true, + stressA2: true, + energyA3: true, + connectionA4: true, + stabilityA5: true, + context: true, + tags: true, + note: true, + noteEncrypted: true, + moodLoggedAt: true, + source: true, + syncVersion: true, + deletedAt: true, + updatedAt: true, +} satisfies Prisma.MoodEntrySelect; + +const INTAKE_SELECT = { + id: true, + medicationId: true, + scheduledFor: true, + takenAt: true, + skipped: true, + source: true, + autoRecorded: true, + syncVersion: true, + deletedAt: true, + updatedAt: true, +} satisfies Prisma.MedicationIntakeEventSelect; + +export type MeasurementRow = Prisma.MeasurementGetPayload<{ + select: typeof MEASUREMENT_SELECT; +}>; +export type MoodRow = Prisma.MoodEntryGetPayload<{ + select: typeof MOOD_SELECT; +}>; +export type IntakeRow = Prisma.MedicationIntakeEventGetPayload<{ + select: typeof INTAKE_SELECT; +}>; +export type CycleDayRow = Prisma.CycleDayLogGetPayload<{ + include: typeof dayLogSymptomInclude; +}>; +export type CycleRow = Prisma.MenstrualCycleGetPayload; + +/** + * Build the `(updatedAt, id)` keyset predicate for a domain's watermark. + * Empty object (full scan) when the client has no watermark for it yet. + */ +function keysetFilter(wm: DomainWatermark | undefined) { + if (!wm) return {}; + const at = new Date(wm.updatedAtMs); + return { + OR: [ + { updatedAt: { gt: at } }, + { AND: [{ updatedAt: at }, { id: { gt: wm.id } }] }, + ], + }; +} + +export interface DomainRows { + measurements: MeasurementRow[]; + mood: MoodRow[]; + intakes: IntakeRow[]; + cycleDays: CycleDayRow[]; + cycles: CycleRow[]; +} + +/** + * Read one page plus one row of every domain past its watermark. The cycle + * walks are skipped outright when the module is off, not filtered after the + * fact, so the disabled rows are never read out of Postgres at all. + */ +export async function readDomainRows( + userId: string, + cursor: SyncCursor, + limit: number, + cycleEnabled: boolean, +): Promise { + const [measurements, mood, intakes, cycleDays, cycles] = await Promise.all([ + prisma.measurement.findMany({ + where: { userId, ...keysetFilter(cursor.measurements) }, + orderBy: [{ updatedAt: "asc" }, { id: "asc" }], + take: limit + 1, + select: MEASUREMENT_SELECT, + }), + prisma.moodEntry.findMany({ + where: { userId, ...keysetFilter(cursor.mood) }, + orderBy: [{ updatedAt: "asc" }, { id: "asc" }], + take: limit + 1, + select: MOOD_SELECT, + }), + prisma.medicationIntakeEvent.findMany({ + where: { userId, ...keysetFilter(cursor.intakes) }, + orderBy: [{ updatedAt: "asc" }, { id: "asc" }], + take: limit + 1, + select: INTAKE_SELECT, + }), + cycleEnabled + ? prisma.cycleDayLog.findMany({ + where: { userId, ...keysetFilter(cursor.cycleDays) }, + orderBy: [{ updatedAt: "asc" }, { id: "asc" }], + take: limit + 1, + include: dayLogSymptomInclude, + }) + : [], + cycleEnabled + ? prisma.menstrualCycle.findMany({ + where: { userId, ...keysetFilter(cursor.cycles) }, + orderBy: [{ updatedAt: "asc" }, { id: "asc" }], + take: limit + 1, + }) + : [], + ]); + return { measurements, mood, intakes, cycleDays, cycles }; +} diff --git a/src/lib/sync/changes/wire.ts b/src/lib/sync/changes/wire.ts new file mode 100644 index 000000000..d4a95ed6b --- /dev/null +++ b/src/lib/sync/changes/wire.ts @@ -0,0 +1,114 @@ +/** + * The wire rows of `GET /api/sync/changes`, one upsert and one tombstone + * shape per domain. Tombstone identity follows the iOS §7.3 table: + * measurements key on `externalId` (the cross-device key); mood and intakes + * key on the server `id` (the client mirrors those by server id); cycle days + * key on `externalId` when present (HealthKit-origin rows are + * externalId-keyed cross-device, like measurements) else the server `id`; + * cycles key on the server `id` (computed rows carry no externalId). + */ +import type { MoodContextWire } from "@/lib/mood/context"; +import type { CycleDayLogDTO, MenstrualCycleDTO } from "@/lib/cycle/dto"; + +export interface MeasurementUpsert { + id: string; + externalId: string | null; + type: string; + value: number; + unit: string; + measuredAt: string; + source: string; + notes: string | null; + syncVersion: number; + updatedAt: string; +} + +export interface MeasurementTombstone { + id: string; + externalId: string | null; + syncVersion: number; + deletedAt: string; + updatedAt: string; +} + +export interface MoodUpsert { + id: string; + date: string; + mood: string; + score: number; + /** + * The five level-A self-state values, each 0-10 and each nullable. `a2` is + * inverse-oriented: a higher number means more stress, and a client that + * wants "up is better" flips the sign itself rather than the server + * storing a flipped value. + * + * Server-authoritative: `a1` is derived from the five-point label on every + * write, so a client that sends only `mood` still mirrors a complete row. + */ + a1: number | null; + a2: number | null; + a3: number | null; + a4: number | null; + a5: number | null; + /** + * The day's context, or null when the entry carries none. Null and an + * object of nulls are different answers and the feed keeps them apart. + */ + context: MoodContextWire | null; + tags: string | null; + note: string | null; + moodLoggedAt: string; + source: string; + syncVersion: number; + updatedAt: string; +} + +/** The tombstone of a domain keyed on the server id alone. */ +export interface IdTombstone { + id: string; + syncVersion: number; + deletedAt: string; + updatedAt: string; +} + +export interface IntakeUpsert { + id: string; + medicationId: string; + scheduledFor: string; + takenAt: string | null; + skipped: boolean; + source: string; + /** Recorded by the server (`autoMarkTaken`), not the person. */ + autoRecorded: boolean; + syncVersion: number; + updatedAt: string; +} + +export interface CycleDayTombstone { + id: string; + externalId: string | null; + syncVersion: number; + deletedAt: string; + updatedAt: string; +} + +export interface DomainBlock { + upserts: U[]; + tombstones: T[]; +} + +export interface SyncChanges { + measurements: DomainBlock; + mood: DomainBlock; + intakes: DomainBlock; + cycleDays: DomainBlock; + cycles: DomainBlock; +} + +export interface SyncChangesBody { + serverNow: string; + cursor: string | null; + hasMore: boolean; + cursorExpired: boolean; + changes: SyncChanges; +} diff --git a/src/lib/workouts/delete-manual-workout.ts b/src/lib/workouts/delete-manual-workout.ts new file mode 100644 index 000000000..2a0a6846c --- /dev/null +++ b/src/lib/workouts/delete-manual-workout.ts @@ -0,0 +1,115 @@ +/** + * `DELETE /api/workouts/{id}`: remove a workout that was entered by hand. + * + * - Ownership: another user's row reads as missing, like the detail read. + * - `MANUAL` only. Every other source is a sync, and the next sync would + * write the row back, so it is refused instead of pretending. + * + * The workout table carries no tombstone, so the row is removed outright; + * route, samples and insight rows cascade with it. The insight's generation + * claim stays behind with a null workout id: it is the day's cap ledger, and + * a delete must not hand the slot back. A personal record the workout set + * would otherwise outlive it (the detector only ever raises a stored best), + * so the records keyed on this workout go in the same transaction and a + * silent detection pass re-derives the honest best. + */ +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import { auditLog } from "@/lib/auth/audit"; +import { invalidateUserMeasurements } from "@/lib/cache/invalidate"; +import { enqueuePrDetection } from "@/lib/jobs/pr-detection"; +import { + lockPersonalRecordsForUser, + workoutPrSlotsForSport, +} from "@/lib/personal-records/pr-detection-worker"; + +export type DeleteManualWorkoutOutcome = + { kind: "not_found" } | { kind: "synced_source" } | { kind: "deleted" }; + +export async function deleteManualWorkout(args: { + userId: string; + id: string; + ipAddress: string | null; +}): Promise { + const { userId, id } = args; + const row = await prisma.workout.findUnique({ + where: { id }, + select: { + id: true, + userId: true, + source: true, + externalId: true, + startedAt: true, + sportType: true, + }, + }); + if (!row || row.userId !== userId) { + return { kind: "not_found" }; + } + if (row.source !== "MANUAL") { + annotate({ + action: { + name: "workout.delete", + entity_type: "workout", + entity_id: id, + }, + meta: { outcome: "synced_source", source: row.source }, + }); + return { kind: "synced_source" }; + } + + const removedRecords = await prisma.$transaction(async (tx) => { + // The detector reads the best workout and writes its record under this + // same lock, so it either wrote before this delete (and the record goes + // below) or reads after it (and never sees the workout). + await lockPersonalRecordsForUser(tx, userId); + const records = await tx.personalRecord.deleteMany({ + where: { + userId: userId, + source: "MANUAL", + ...(row.externalId !== null + ? { metricSlot: { not: null }, externalId: row.externalId } + : { + // A legacy record carries no external id, so the start time + // is all that ties it to this workout. Scope it to the slots + // of this workout's sport so a record another sport set at + // the same instant stays. + metricSlot: { in: workoutPrSlotsForSport(row.sportType) }, + externalId: null, + achievedAt: row.startedAt, + }), + }, + }); + await tx.workout.delete({ where: { id: row.id } }); + return records.count; + }); + + await auditLog("workout.delete", { + userId: userId, + ipAddress: args.ipAddress, + details: { + workoutId: id, + sportType: row.sportType, + personalRecordsRemoved: removedRecords, + }, + }); + annotate({ + action: { name: "workout.delete", entity_type: "workout", entity_id: id }, + meta: { outcome: "deleted", personalRecordsRemoved: removedRecords }, + }); + + invalidateUserMeasurements(userId, { evict: true }); + + if (removedRecords > 0) { + try { + await enqueuePrDetection(userId, { silent: true }); + } catch (err) { + annotate({ + action: { name: "personal_records.detection_enqueue_failed" }, + meta: { error: err instanceof Error ? err.message : String(err) }, + }); + } + } + + return { kind: "deleted" }; +} diff --git a/src/lib/workouts/detail.ts b/src/lib/workouts/detail.ts new file mode 100644 index 000000000..ed1d6e026 --- /dev/null +++ b/src/lib/workouts/detail.ts @@ -0,0 +1,407 @@ +/** + * The single-workout detail behind `GET /api/workouts/{id}`: the stored row, + * the canonical winner of its duplicate cluster, the enrichment reads (heart + * rate curve, effort zones, splits, sport context, the previous session) and + * the stored Activity Insight while it may be served. The route owns + * authentication, the module gate and the AI capability. + */ +import { prisma } from "@/lib/db"; +import { annotate } from "@/lib/logging/context"; +import { + canonicalClusterOf, + canonicalWorkoutsInRange, + readWorkoutsAroundRange, + type CanonicalReadRow, + type WorkoutBoundedRead, + type WorkoutStartBounds, +} from "@/lib/workouts/canonical-read"; +import type { aiCapabilityToServe } from "@/lib/ai/capabilities/gate"; +import { getAgeFromDateOfBirth } from "@/lib/analytics/pulse-targets"; +import { decryptFromBytes } from "@/lib/ai/coach/bytes-codec"; +import { buildWorkoutHrSeries } from "@/lib/workouts/hr-series"; +import { + computeZones, + hrMaxFromAge, + parseWhoopZoneDurations, +} from "@/lib/workouts/zones"; +import { computeSplits } from "@/lib/workouts/splits"; +import { buildSportContext } from "@/lib/workouts/sport-context"; +import type { RouteCoordinate } from "@/lib/workouts/route-svg"; +import { readRouteGeometry } from "@/lib/workouts/route-geometry-cipher"; +import { resolveUserTimezone } from "@/lib/tz/resolver"; +import { userDayKey } from "@/lib/tz/format"; + +type AiState = Awaited>; + +/** How many earlier rows one step of the previous-session search reads. */ +const PREVIOUS_SESSION_BATCH = 20; +/** Steps before the search gives up; each reaches past the last one. */ +const PREVIOUS_SESSION_MAX_STEPS = 5; + +/** + * The newest canonical workout of the sport that starts before `before` (the + * first start of the current session's cluster). A batch of earlier rows names + * the span to resolve; the span is read through the windowed reader so its + * twins are resolved the way the list resolves them. A span whose rows are all + * hidden by a twin further back moves the search back one batch. + */ +async function previousCanonicalWorkoutId( + read: WorkoutBoundedRead, + latestStartsBefore: (to: Date, take: number) => Promise, + before: Date, + priorityJson: unknown, +): Promise { + let to = before; + for (let step = 0; step < PREVIOUS_SESSION_MAX_STEPS; step += 1) { + const earlier = await latestStartsBefore(to, PREVIOUS_SESSION_BATCH); + if (earlier.length === 0) return null; + const from = earlier[earlier.length - 1]!; + const windowed = await readWorkoutsAroundRange({ from, to: before }, read); + const canonical = canonicalWorkoutsInRange(windowed, priorityJson); + const newest = canonical[canonical.length - 1]; + if (newest) return newest.id; + to = from; + } + return null; +} + +/** The stored workout with its track, series and (when served) insight. */ +async function loadWorkout(id: string, ai: AiState) { + return prisma.workout.findUnique({ + where: { id }, + include: { + route: { + select: { + id: true, + geometry: true, + geometryEncrypted: true, + sampleTimestamps: true, + createdAt: true, + }, + }, + samples: { + select: { + samples: true, + sampleCount: true, + }, + }, + insight: ai.available + ? { + select: { paragraphEncrypted: true, generatedAt: true }, + } + : false, + }, + }); +} + +type StoredWorkout = NonNullable>>; + +/** + * The detail body for a workout the caller owns, or null when the id names + * no workout of theirs (another account's row reads as missing, so the + * existence channel stays sealed). + */ +export async function readWorkoutDetail(args: { + userId: string; + id: string; + compact: boolean; + ai: AiState; +}) { + const { userId, id, compact, ai } = args; + const row = await loadWorkout(id, ai); + if (!row || row.userId !== userId) return null; + const identity = await resolveIdentity(userId, row); + const enrichment = await readEnrichment(userId, row, identity); + return shapeDetail(row, identity, enrichment, { compact, ai }); +} + +/** The canonical winner of the row's cluster, and the local day. */ +async function resolveIdentity(userId: string, row: StoredWorkout) { + // Resolve the canonical winner for the row's cluster. The read goes + // through the same windowed reader as the list, so the cluster drawn here + // is the one the list draws: a twin that straddles a clock slot boundary, + // or one whose cluster begins with an earlier workout, resolves to the row + // the list shows. + const readSameSport = (bounds: WorkoutStartBounds) => + prisma.workout.findMany({ + where: { + userId, + sportType: row.sportType, + startedAt: bounds, + }, + orderBy: [{ startedAt: "asc" }, { id: "asc" }], + select: { + id: true, + source: true, + startedAt: true, + sportType: true, + // Part of the canonical session identity consulted by the picker — + // without it a HealthKit re-send of this very workout stays in the + // cluster and the deep link can resolve to the duplicate instead of + // the row the list shows. + durationSec: true, + avgHeartRate: true, + maxHeartRate: true, + metadata: true, + }, + }); + const around = await readWorkoutsAroundRange( + { from: row.startedAt, to: row.startedAt, inclusiveEnd: true }, + readSameSport, + ); + + const userRow = await prisma.user.findUnique({ + where: { id: userId }, + select: { sourcePriorityJson: true, dateOfBirth: true }, + }); + // The local calendar day this session belongs to. Resolved here, in the + // user's timezone, so the day linkage stays server-authoritative — a + // client that derived it from `startedAt` would fall back to the browser + // zone and put a late-evening session on the wrong day whenever the two + // disagree. + const timezone = await resolveUserTimezone(userId); + const dayKey = userDayKey(row.startedAt, timezone); + + const cluster = canonicalClusterOf( + around, + row.id, + userRow?.sourcePriorityJson ?? null, + ); + // The cluster's canonical rows, the requested row first when it is one of + // them and otherwise the nearest in time, so a deep link to a hidden twin + // lands on the most similar row. + const canonical = cluster?.canonical[0] ?? null; + const effectiveAvgHeartRate = canonical?.avgHeartRate ?? row.avgHeartRate; + const effectiveMaxHeartRate = canonical?.maxHeartRate ?? row.maxHeartRate; + + return { + readSameSport, + userRow, + dayKey, + cluster, + canonical, + effectiveAvgHeartRate, + effectiveMaxHeartRate, + }; +} + +async function readEnrichment( + userId: string, + row: StoredWorkout, + identity: Awaited>, +) { + const { readSameSport, userRow, cluster, canonical } = identity; + // ── Enrichment reads (all over existing tables — no migration) ──── + + // Heart-rate curve: stored series first, pulse-window fallback + // second, hide third. One server path, one DTO with provenance. + const hrSeries = await buildWorkoutHrSeries({ + userId, + startedAt: row.startedAt, + endedAt: row.endedAt, + durationSec: row.durationSec, + storedSamples: row.samples?.samples ?? null, + }); + annotate({ + action: { name: "workouts.detail.hr_series" }, + meta: { + source: hrSeries?.source ?? "none", + points: hrSeries?.points.length ?? 0, + }, + }); + + // Effort zones: WHOOP device durations win; else %HRmax fold from + // the series when profile age exists; else null. + const ageYears = getAgeFromDateOfBirth(userRow?.dateOfBirth ?? null); + const zones = computeZones({ + hrMax: hrMaxFromAge(ageYears), + series: hrSeries?.points ?? [], + bucketSec: hrSeries?.bucketSec ?? 0, + whoopZoneDurations: parseWhoopZoneDurations( + canonical?.metadata ?? row.metadata, + ), + }); + + // Per-km splits computed server-side from the geometry + timestamps + // so the web client keeps dropping the raw timestamp blob under + // `compact=1` while still rendering splits (server-authoritative + // parity — iOS gets the same resolved figures). + // The track is stored sealed; the legacy readable column is + // read only for a row the backfill has not reached yet. + const routeGeometry = row.route ? readRouteGeometry(row.route) : null; + const geometryCoords = + routeGeometry && + typeof routeGeometry === "object" && + Array.isArray((routeGeometry as { coordinates?: unknown }).coordinates) + ? (routeGeometry as { coordinates: RouteCoordinate[] }).coordinates + : null; + const splits = + geometryCoords && Array.isArray(row.route?.sampleTimestamps) + ? computeSplits(geometryCoords, row.route.sampleTimestamps as string[]) + : null; + + // Sport context: the user's own last-180-days average for this + // sport, cross-source-collapsed so twins don't double-count. + const sportContext = await buildSportContext( + userId, + row.sportType, + userRow?.sourcePriorityJson ?? null, + row.id, + ); + + // The session before this one in the same sport. "How does this compare + // to last time?" is the most likely next question after reading a + // workout, and today it costs a trip back to the list plus a scan. + // Scoped to rows that start before this session's cluster, so a twin of + // THIS session can never be offered as the previous one, and resolved + // through the same windowed read so the link lands on the row the list + // shows. + const clusterStart = cluster?.members[0]?.startedAt ?? row.startedAt; + const previousWorkoutId = await previousCanonicalWorkoutId( + readSameSport, + async (to, take) => + ( + await prisma.workout.findMany({ + where: { + userId, + sportType: row.sportType, + startedAt: { lt: to }, + }, + orderBy: [{ startedAt: "desc" }, { id: "desc" }], + take, + select: { startedAt: true }, + }) + ).map((earlier) => earlier.startedAt), + clusterStart, + userRow?.sourcePriorityJson ?? null, + ); + return { + hrSeries, + zones, + routeGeometry, + splits, + sportContext, + previousWorkoutId, + }; +} + +function shapeDetail( + row: StoredWorkout, + identity: Awaited>, + enrichment: Awaited>, + options: { compact: boolean; ai: AiState }, +) { + const { compact, ai } = options; + const { dayKey, canonical, effectiveAvgHeartRate, effectiveMaxHeartRate } = + identity; + const { + hrSeries, + zones, + routeGeometry, + splits, + sportContext, + previousWorkoutId, + } = enrichment; + // A route whose track cannot be read (a sealed value that no longer + // opens, or an old row stored as a JSON null) is reported as no route at + // all: clients decode `route.geometry` as required, so a route object + // with a null geometry would fail the whole workout detail for them. + const route = + row.route && routeGeometry + ? { + geometry: routeGeometry, + // `compact=1` drops the (up to 20k-entry) timestamp array; the + // SVG needs geometry, not the per-sample timestamps, and the + // splits above are already derived from them server-side. + sampleTimestamps: compact + ? null + : (row.route.sampleTimestamps ?? null), + } + : null; + + // Route-independent per-workout HR series. Present for + // both indoor (no route) and outdoor workouts that shipped a + // `samples` array on ingest; null otherwise. `compact=1` keeps the + // denormalised count but drops the raw sample blob (the web curve + // reads `hrSeries` instead). + const samples = row.samples + ? { + sampleCount: row.samples.sampleCount, + samples: compact ? null : row.samples.samples, + } + : null; + + // Decrypt the stored paragraph. Fail-SOFT here and only here: `decrypt` is + // fail-closed by design, so a rotated-away key would otherwise turn the + // whole workout-detail page into a 500 over a garnish field. An + // undecryptable paragraph degrades to no card, exactly like a workout that + // never had one. + let aiInsight: { paragraph: string; generatedAt: string } | null = null; + if (ai.available && row.insight) { + try { + aiInsight = { + paragraph: decryptFromBytes(row.insight.paragraphEncrypted), + generatedAt: row.insight.generatedAt.toISOString(), + }; + } catch { + annotate({ + action: { name: "workouts.detail.insight_undecryptable" }, + meta: { workoutId: row.id }, + }); + } + } + + return { + id: row.id, + sportType: row.sportType, + startedAt: row.startedAt, + endedAt: row.endedAt, + // `YYYY-MM-DD` in the user's timezone. Additive; it addresses the + // day-scoped reads (intraday pulse, that night's sleep, the day's + // mood) the detail surface renders around the session. + dayKey, + durationSec: row.durationSec, + distanceM: row.totalDistanceM, + activeEnergyKcal: row.totalEnergyKcal, + avgHr: effectiveAvgHeartRate, + maxHr: effectiveMaxHeartRate, + minHr: row.minHeartRate, + // The row's own heart rate. `avgHr` / `maxHr` above may be filled in + // from a twin by the canonical picker, which is right for display and + // wrong for an edit: re-posting them would store the twin's values on + // the hand-entered row (#1162). + storedAvgHr: row.avgHeartRate, + storedMaxHr: row.maxHeartRate, + stepCount: row.stepCount, + elevationM: row.elevationM, + pauseDurationSec: row.pauseDurationSec, + source: row.source, + externalId: row.externalId, + metadata: row.metadata, + route, + samples, + // #67 enrichment — all additive, all over existing tables. + hrSeries, + zones, + splits, + sportContext, + // The per-workout Activity Insight. A pure READ of a row the + // `workout-insight-generate` worker wrote when this workout landed — + // this route never generates, never enqueues, and never falls back to + // a provider. A workout with no row (every historical one, every + // re-synced one, every one on a provider-less install) serves null and + // the page's `{aiInsight ? : null}` renders nothing. + aiInsight, + // Whether the Activity Insight can be shown, and why not. + ai, + // When the requested id is a non-canonical twin the + // caller can redirect to `canonicalId` to land on the cluster + // winner. `canonicalId === id` when the requested row already + // is the winner. + canonicalId: canonical?.id ?? row.id, + // The canonical previous session in this sport, or null when this is + // the first one on record. Additive; a client that does not read it is + // unaffected. + previousWorkoutId, + }; +} diff --git a/tests/integration/__golden__/status-parity/batch.json b/tests/integration/__golden__/status-parity/batch.json index 538a0ae4c..054f02094 100644 --- a/tests/integration/__golden__/status-parity/batch.json +++ b/tests/integration/__golden__/status-parity/batch.json @@ -2,91 +2,9 @@ "batch-en": { "calls": [ { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "BATCHED OUTPUT — you are assessing SEVERAL of this user's metrics in one pass. Each metric below carries its OWN graded snapshot under a \"### \" heading; assess each one STRICTLY from its own snapshot block, applying every rule above per metric. Do NOT let one metric's finding leak into another. Do NOT invent a metric: assess ONLY the keys present below.", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. \"perMetric\" carries one short assessment (2-4 sentences each, the same contract as a single card) per present key:", - "{ \"perMetric\": { \"bp\": \"...\", \"pulse\": \"...\", \"weight\": \"...\", \"bmi\": \"...\", \"mood\": \"...\", \"compliance\": \"...\", \"general\": \"...\" } }", - "Include a key ONLY if its snapshot block is present below (keys present: bp, pulse, weight, bmi, mood, compliance, general). Omit any metric whose block is absent — never fabricate one." - ], + "maxTokens": 1400, "messages": [ { - "role": "user", "content": [ "### bp", "", @@ -227,16 +145,121 @@ "", "", "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" - ] + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 1400, - "seed": 1618033, "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "BATCHED OUTPUT — you are assessing SEVERAL of this user's metrics in one pass. Each metric below carries its OWN graded snapshot under a \"### \" heading; assess each one STRICTLY from its own snapshot block, applying every rule above per metric. Do NOT let one metric's finding leak into another. Do NOT invent a metric: assess ONLY the keys present below.", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. \"perMetric\" carries one short assessment (2-4 sentences each, the same contract as a single card) per present key:", + "{ \"perMetric\": { \"bp\": \"...\", \"pulse\": \"...\", \"weight\": \"...\", \"bmi\": \"...\", \"mood\": \"...\", \"compliance\": \"...\", \"general\": \"...\" } }", + "Include a key ONLY if its snapshot block is present below (keys present: bp, pulse, weight, bmi, mood, compliance, general). Omit any metric whose block is absent — never fabricate one." + ], + "temperature": 0.45, "timeoutMs": 60000 }, { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"your mood\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -322,85 +345,55 @@ "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with what this means for the user today, then the number behind it.", - "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"your mood\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "served": 0, - "batched": 6, - "fellBack": 1, - "batchCallMade": true - }, "notes": [ { - "userId": "parity-rich", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", "metric": "blood-pressure", - "locale": "en", - "text": "Batched parity note for bp.", - "items": null, - "inputHash": null, + "negativeReason": null, + "retryAt": null, "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "text": "Batched parity note for bp.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Batched parity note for bmi.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Batched parity note for general.", "items": null, - "inputHash": null, - "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", + "locale": "en", + "metric": "bmi", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Batched parity note for bmi.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "medication-compliance", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, "locale": "en", - "text": "Batched parity note for compliance.", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "text": "Batched parity note for general.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, "items": [ { "medicationId": "parity-rich-med-bp", @@ -411,367 +404,67 @@ "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." } ], - "inputHash": null, - "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", + "text": "Batched parity note for compliance.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Batched parity note for pulse.", - "items": null, "inputHash": null, - "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Batched parity note for weight.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, "retryAt": null, - "negativeReason": null - } - ] - }, - "batch-unchanged-en": { - "calls": [ + "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", + "text": "Batched parity note for pulse.", + "userId": "parity-rich" + }, { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — MOOD / WELL-BEING:", - "- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).", - "- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", - "- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.", - "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", - "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", - "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", - "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with what this means for the user today, then the number behind it.", - "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"your mood\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Batched parity note for weight.", + "userId": "parity-rich" } ], - "enqueued": [], "result": { - "served": 6, - "batched": 0, + "batchCallMade": true, + "batched": 6, "fellBack": 1, - "batchCallMade": false - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Batched parity note for bp.", - "items": null, - "inputHash": null, - "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Batched parity note for bmi.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Batched parity note for general.", - "items": null, - "inputHash": null, - "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Batched parity note for compliance.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "inputHash": null, - "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for batch-unchanged-en.", - "items": null, - "inputHash": null, - "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Batched parity note for pulse.", - "items": null, - "inputHash": null, - "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Batched parity note for weight.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] + "served": 0 + } }, "batch-failed-de": { "calls": [ { - "system": [ - "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", - "", - "DEINE DATENGRUNDLAGE (graded snapshot):", - "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", - "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", - "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", - "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", - "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", - "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", - "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", - "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", - "", - "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", - "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", - "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", - "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", - "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", - "(d) einen Punkt zum Im-Auge-behalten;", - "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", - "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", - "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", - "", - "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", - "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", - "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", - "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", - "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", - "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", - "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", - "", - "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", - "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", - "", - "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", - "", - "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", - "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", - "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", - "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", - "", - "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", - "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", - "", - "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", - "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", - "", - "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", - "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", - "", - "AKUTE WARNZEICHEN (Vertrag, kein Stil)", - "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", - "", - "ERDUNG — NULL HALLUZINATIONEN", - "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", - "", - "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", - "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", - "", - "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", - "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", - "", - "EINORDNEN, NICHT AUFZÄHLEN", - "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", - "", - "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", - "", - "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", - "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", - "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", - "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", - "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", - "", - "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", - "{ \"summary\": \"...\" }", - "", - "GEBÜNDELTE AUSGABE — du bewertest in einem Durchgang MEHRERE Metriken dieses Nutzers. Jede Metrik unten trägt ihren EIGENEN graded snapshot unter einer Überschrift \"### \"; bewerte jede ausschließlich aus ihrem eigenen Snapshot-Block und wende alle obigen Regeln pro Metrik an. Lass den Befund einer Metrik NICHT in eine andere überlaufen. Erfinde KEINE Metrik: bewerte NUR die unten vorhandenen Keys.", - "", - "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. \"perMetric\" enthält je vorhandenem Key eine kurze Einschätzung (je 2-4 Sätze, derselbe Vertrag wie eine Einzelkarte):", - "{ \"perMetric\": { \"bp\": \"...\", \"pulse\": \"...\", \"weight\": \"...\", \"bmi\": \"...\", \"mood\": \"...\", \"compliance\": \"...\", \"general\": \"...\" } }", - "Nimm einen Key NUR auf, wenn sein Snapshot-Block unten vorhanden ist (vorhandene Keys: bp, pulse, weight, bmi, mood, compliance, general). Lass jede Metrik ohne Block weg — erfinde niemals eine." - ], + "maxTokens": 1400, "messages": [ { - "role": "user", "content": [ "### bp", "", @@ -912,16 +605,128 @@ "", "", "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" - ] + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 1400, - "seed": 1618033, "responseFormat": "json", + "seed": 1618033, + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "GEBÜNDELTE AUSGABE — du bewertest in einem Durchgang MEHRERE Metriken dieses Nutzers. Jede Metrik unten trägt ihren EIGENEN graded snapshot unter einer Überschrift \"### \"; bewerte jede ausschließlich aus ihrem eigenen Snapshot-Block und wende alle obigen Regeln pro Metrik an. Lass den Befund einer Metrik NICHT in eine andere überlaufen. Erfinde KEINE Metrik: bewerte NUR die unten vorhandenen Keys.", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. \"perMetric\" enthält je vorhandenem Key eine kurze Einschätzung (je 2-4 Sätze, derselbe Vertrag wie eine Einzelkarte):", + "{ \"perMetric\": { \"bp\": \"...\", \"pulse\": \"...\", \"weight\": \"...\", \"bmi\": \"...\", \"mood\": \"...\", \"compliance\": \"...\", \"general\": \"...\" } }", + "Nimm einen Key NUR auf, wenn sein Snapshot-Block unten vorhanden ist (vorhandene Keys: bp, pulse, weight, bmi, mood, compliance, general). Lass jede Metrik ohne Block weg — erfinde niemals eine." + ], + "temperature": 0.45, "timeoutMs": 60000 }, { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit dem Gesamteindruck in klaren Worten und bring die Zahl danach als Beleg — nicht zahlen-zuerst.", + "Schreibe eine kurze Einschätzung zum Blutdruck dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Gesamteindruck, nicht der Zahl (z. B. \"ruhig und genau da, wo du ihn haben willst\", \"diese Woche einen Tick höher als sonst\") — und bring danach die systolischen/diastolischen Werte als Beleg, als EIN Paar beurteilt und gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit den Werten. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 94 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der Blutzucker höher liegt, scheint der systolische Blutdruck am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=23, r=0.91)", + "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"deine Systole\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"deine Diastole\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -1010,40 +815,35 @@ "- Stimmung: moodContext.moodVsSystolicCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4.", "- Eine Botschaft: Wenn die Werte über der Baseline liegen, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage zur selben Uhrzeit nachmessen, oder — bei lückenhafter Einnahmetreue — die Einnahme verlässlicher machen). Bei stabilen Werten im Ziel: das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." ], + "temperature": 0.45, + "timeoutMs": 60000 + }, + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Datum: 2026-06-15", - "OPENER HINT: Eröffne mit dem Gesamteindruck in klaren Worten und bring die Zahl danach als Beleg — nicht zahlen-zuerst.", - "Schreibe eine kurze Einschätzung zum Blutdruck dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Gesamteindruck, nicht der Zahl (z. B. \"ruhig und genau da, wo du ihn haben willst\", \"diese Woche einen Tick höher als sonst\") — und bring danach die systolischen/diastolischen Werte als Beleg, als EIN Paar beurteilt und gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit den Werten. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zum BMI dieser Person. Beginne mit der BEDEUTUNG der Bandlage in klaren Worten — wo er liegt und ob das hält oder sich verschiebt, nicht der Zahl (z. B. \"liegt bequem im gesunden Band\", \"nähert sich dem nächsten Band\") — und bring danach den aktuellen Wert und das WHO-Band als Beleg, samt Angabe gegen die eigene Wochen-/Monats-Baseline, ob er ein Band gewechselt hat oder sich einer Bandgrenze nähert; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Das Gewichtstempo bleibt der Gewichts-Karte überlassen. Konfidenz aus Messanzahl und Aktualität ableiten.", "", "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", "", "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", "", - "DATENLAGE: 94 Tages-Buckets im Fenster, letzter Wert heute.", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", - "", - "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Wenn der Blutzucker höher liegt, scheint der systolische Blutdruck am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=23, r=0.91)", - "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", - "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", "", "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"deine Systole\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"deine Diastole\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" - ] + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"dein BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - }, - { + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -1128,35 +928,33 @@ "- Grenzen des BMI offen ansprechen: Er misst keine Körperzusammensetzung und ist bei muskulösen oder sehr sportlichen Personen wenig aussagekräftig.", "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, der zur Bandlage passt, wenn der Befund einen nahelegt — bei stabilem Wert im günstigen Band das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." ], + "temperature": 0.45, + "timeoutMs": 60000 + }, + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Datum: 2026-06-15", "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", - "Schreibe eine kurze Einschätzung zum BMI dieser Person. Beginne mit der BEDEUTUNG der Bandlage in klaren Worten — wo er liegt und ob das hält oder sich verschiebt, nicht der Zahl (z. B. \"liegt bequem im gesunden Band\", \"nähert sich dem nächsten Band\") — und bring danach den aktuellen Wert und das WHO-Band als Beleg, samt Angabe gegen die eigene Wochen-/Monats-Baseline, ob er ein Band gewechselt hat oder sich einer Bandgrenze nähert; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Das Gewichtstempo bleibt der Gewichts-Karte überlassen. Konfidenz aus Messanzahl und Aktualität ableiten.", + "Schreibe eine kurze Gesamteinschätzung über die verfügbaren Gesundheitsdaten. Beginne mit dem Gesamteindruck in klaren Worten — wie es zusammengenommen aussieht, nicht mit einer Zahl (z. B. \"über alles hinweg eine ruhige Phase\", \"diese Woche sticht eine Sache heraus\") — und bring danach die ein bis zwei auffälligsten Metriken als Beleg, jeweils gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit einem Wert. Schließe nur dann mit dem einen wichtigsten machbaren Schritt, wenn das Gesamtbild wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl, Dichte und Aktualität ableiten.", "", "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", "", "", "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", "", - "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "DATENLAGE: 887 Tages-Buckets im Fenster, letzter Wert heute.", "", "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"dein BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" - ] + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - }, - { + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -1242,33 +1040,40 @@ "- Zusammenhänge nur erwähnen, wenn ein r-Wert im Snapshot vorhanden und |r| > 0.4 ist — als Zusammenhang, nie als Ursache. Stimmung als Kontext einbeziehen, nicht übergewichten.", "- Eine Botschaft: Auch über alle Metriken hinweg endet die Einschätzung NUR DANN mit GENAU EINER wichtigsten, machbaren Empfehlung (\"eine Sache\"), wenn das Gesamtbild eine nahelegt. Ist alles stabil und gibt es nichts Sinnvolles zu tun, sage das ehrlich und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt eine Empfehlung zu erzwingen." ], + "temperature": 0.45, + "timeoutMs": 60000 + }, + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Datum: 2026-06-15", - "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", - "Schreibe eine kurze Gesamteinschätzung über die verfügbaren Gesundheitsdaten. Beginne mit dem Gesamteindruck in klaren Worten — wie es zusammengenommen aussieht, nicht mit einer Zahl (z. B. \"über alles hinweg eine ruhige Phase\", \"diese Woche sticht eine Sache heraus\") — und bring danach die ein bis zwei auffälligsten Metriken als Beleg, jeweils gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit einem Wert. Schließe nur dann mit dem einen wichtigsten machbaren Schritt, wenn das Gesamtbild wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl, Dichte und Aktualität ableiten.", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zum Gewicht dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — Richtung und Tempo, nicht der Zahl (z. B. \"geht ruhig nach unten\", \"hält sich genau da, wo es sich eingependelt hat\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, als kontinuierlichen Trend und Tempo gegen die eigene Wochen-/Monats-Baseline gelesen (Tempo pro Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt); führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", "", "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", "", "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", "", - "DATENLAGE: 887 Tages-Buckets im Fenster, letzter Wert heute.", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", + "- Wenn die Schlafdauer höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=45, r=0.98)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", "", "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" - ] + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"dein Gewicht\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - }, - { + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -1357,40 +1162,33 @@ "- Zusammenhänge: weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation und moodContext.moodVsWeightCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. bei einem Plateau zur selben Tageszeit wiegen, um den echten Trend zu sehen, statt am Tagesrauschen zu hängen). Ist der Trend stabil und gibt es nichts Sinnvolles zu tun, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen." ], + "temperature": 0.45, + "timeoutMs": 60000 + }, + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Datum: 2026-06-15", - "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", - "Schreibe eine kurze Einschätzung zum Gewicht dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — Richtung und Tempo, nicht der Zahl (z. B. \"geht ruhig nach unten\", \"hält sich genau da, wo es sich eingependelt hat\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, als kontinuierlichen Trend und Tempo gegen die eigene Wochen-/Monats-Baseline gelesen (Tempo pro Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt); führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "OPENER HINT: Eröffne mit der Richtung der Veränderung über die Zeit und nenne dann den aktuellen Stand.", + "Schreibe eine kurze Einschätzung zur Einnahmetreue dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — wie verlässlich die Routine aussieht, nicht der Zahl (z. B. \"deine Routine sitzt richtig gut\", \"zuletzt sind ein paar Dosen durchgerutscht\") — und bring danach die jüngste Rate als Beleg, gegen die eigene Baseline eingeordnet (compliance7 vs. compliance30); führe nie mit dem Wert. Schließe nur dann mit einem machbaren, wertfreien Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Ereignisanzahl und Aktualität ableiten.", "", "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", "", "", "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", "", - "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", - "", - "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", - "- Wenn die Schlafdauer höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=45, r=0.98)", - "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "DATENLAGE: 176 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", "", "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"dein Gewicht\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" - ] + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - }, - { + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -1476,33 +1274,33 @@ "- Zusammenhänge zu Vitalwerten nur erwähnen, wenn ein r-Wert im Snapshot vorhanden und |r| > 0.4 ist — als Zusammenhang, nie als Ursache. Keine Wirkung erfinden, wenn die Daten fehlen.", "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. Abenddosen werden häufiger vergessen — eine feste Routine oder ein Reminder zur kritischen Uhrzeit kann helfen). Bei durchgehend hoher Treue das ehrlich anerkennen, statt einen Schritt zu erzwingen. Keine Schuldzuweisung." ], + "temperature": 0.45, + "timeoutMs": 60000 + }, + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Datum: 2026-06-15", - "OPENER HINT: Eröffne mit der Richtung der Veränderung über die Zeit und nenne dann den aktuellen Stand.", - "Schreibe eine kurze Einschätzung zur Einnahmetreue dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — wie verlässlich die Routine aussieht, nicht der Zahl (z. B. \"deine Routine sitzt richtig gut\", \"zuletzt sind ein paar Dosen durchgerutscht\") — und bring danach die jüngste Rate als Beleg, gegen die eigene Baseline eingeordnet (compliance7 vs. compliance30); führe nie mit dem Wert. Schließe nur dann mit einem machbaren, wertfreien Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Ereignisanzahl und Aktualität ableiten.", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zum Ruhepuls dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil, genau da, wo er war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", "", "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", "", "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", "", - "DATENLAGE: 176 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", + "DATENLAGE: 122 Tages-Buckets im Fenster, letzter Wert heute.", "", "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" - ] + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"dein Ruhepuls\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - }, - { + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -1587,33 +1385,33 @@ "- Medikamenten-Bezug: Betablocker senken die Herzfrequenz. moodContext.moodVsPulseCorrelation und ein pulseVsSystolic-Zusammenhang nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", "- Eine Botschaft: Liegt der Ruhepuls über der eigenen Baseline, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage morgens vor dem Aufstehen messen, um eine saubere Ruhe-Baseline zu bekommen). Verbessert er sich oder ist er stabil, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen." ], + "temperature": 0.45, + "timeoutMs": 60000 + }, + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Datum: 2026-06-15", - "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", - "Schreibe eine kurze Einschätzung zum Ruhepuls dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil, genau da, wo er war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zur Stimmung dieser Person. Beginne mit der BEDEUTUNG des jüngsten Musters in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"eine hellere Phase als sonst\", \"stabil, hält sich, wo sie war\") — und bring danach das jüngste Niveau als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem freundlichen, machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Eintragsanzahl und Aktualität ableiten.", "", "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", "", "", "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", "", - "DATENLAGE: 122 Tages-Buckets im Fenster, letzter Wert heute.", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", "", "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"dein Ruhepuls\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" - ] + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"deine Stimmung\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - }, - { + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -1699,137 +1497,107 @@ "- Cross-Metrik: crossMetricContext trägt Korrelationen zu Gewicht, Blutdruck und Puls; nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache. Erzwinge keinen Querverweis ohne klares Muster.", "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren, freundlichen Schritt, wenn der Befund einen nahelegt (z.B. an guten Tagen kurz festhalten, was geholfen hat). Bei stabil guter Stimmung das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", - "Schreibe eine kurze Einschätzung zur Stimmung dieser Person. Beginne mit der BEDEUTUNG des jüngsten Musters in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"eine hellere Phase als sonst\", \"stabil, hält sich, wo sie war\") — und bring danach das jüngste Niveau als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem freundlichen, machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Eintragsanzahl und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"deine Stimmung\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "served": 0, - "batched": 0, - "fellBack": 7, - "batchCallMade": true - }, "notes": [ { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "blood-pressure", + "negativeReason": "error", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Batched parity note for bp.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "text": "Batched parity note for bp.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "bmi", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "bmi", + "negativeReason": "error", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Batched parity note for bmi.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "text": "Batched parity note for bmi.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "general", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "general", + "negativeReason": "error", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Batched parity note for general.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, + "retryAt": null, "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "text": "Batched parity note for general.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "medication-compliance", + "negativeReason": "error", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Batched parity note for compliance.", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, "items": [ { "medicationId": "parity-rich-med-bp", @@ -1840,91 +1608,323 @@ "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." } ], - "inputHash": null, + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "text": "Batched parity note for compliance.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "mood", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "mood", + "negativeReason": "error", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for batch-unchanged-en.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, + "retryAt": null, "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "text": "Parity note for batch-unchanged-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "pulse", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "pulse", + "negativeReason": "error", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Batched parity note for pulse.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "text": "Batched parity note for pulse.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "weight", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "weight", + "negativeReason": "error", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Batched parity note for weight.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Batched parity note for weight.", + "userId": "parity-rich" } - ] + ], + "result": { + "batchCallMade": true, + "batched": 0, + "fellBack": 7, + "served": 0 + } + }, + "batch-unchanged-en": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"your mood\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MOOD / WELL-BEING:", + "- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).", + "- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.", + "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", + "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", + "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", + "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", + "text": "Batched parity note for bp.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Batched parity note for bmi.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "text": "Batched parity note for general.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", + "text": "Batched parity note for compliance.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", + "text": "Parity note for batch-unchanged-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", + "text": "Batched parity note for pulse.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Batched parity note for weight.", + "userId": "parity-rich" + } + ], + "result": { + "batchCallMade": false, + "batched": 0, + "fellBack": 1, + "served": 6 + } } } diff --git a/tests/integration/__golden__/status-parity/biomarker.json b/tests/integration/__golden__/status-parity/biomarker.json index 7f090a0ee..9a7f5299a 100644 --- a/tests/integration/__golden__/status-parity/biomarker.json +++ b/tests/integration/__golden__/status-parity/biomarker.json @@ -1,568 +1,152 @@ { - "read-only-cold": { + "bare-error-de": { "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "preparing": true, - "revalidating": false - }, - "notes": [] - }, - "fresh-en": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — LAB MARKER:", - "- DIFFERENT DATA SHAPE: this snapshot carries NO signal block. The base instruction's fields that reference signal do not apply here. Instead: marker (name, unit, referenceRange), dataCoverage, latest (value, takenAt, rangeStatus) and series (ascending, each { day, value }).", - "- Lead from latest.rangeStatus — the pre-computed placement against the reference range (inside/below/above it, or no range on file) — and from the direction across series. Both are already determined; STATE them, do NOT recompute them.", - "- Their own history leads: how this value sits against this person's own recent draws is the message. The reference range is a coarse secondary anchor, not a verdict — a value slightly outside the band that has held steady for years is a different story from the same value after a clear step.", - "- Steadiness is itself a finding: when the value barely moves across draws, name that constancy — for a lab marker it is the most common and most useful thing to say.", - "- Single draws vary (lab, time of day, fasting state). Do not build a trend out of TWO points; when the history is short, say honestly that it is too little for a direction yet.", - "- NO diagnosis, no attribution of cause, no medication or supplement recommendation. For clearly notable values, refer neutrally to a clinician for interpretation — that is the close, not an invented self-help step.", - "- One message: close with ONE doable step only when the finding genuinely implies one (e.g. take the value to the next check-up). When it is steady and in range, acknowledge that rather than manufacturing a step.", - "- The marker under assessment is \"LDL Cholesterol\"." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", - "Write one short assessment of this person's lab marker. Open with how the marker SITS in plain words — the read, not the number (e.g. \"holding steady just inside the band\", \"sitting a step above where it usually runs\") — then bring in ONE concrete figure from the snapshot right after as support; never lead with the value. Place it against this person's own recent draws first and the reference range only as a coarse anchor. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the number of draws and their recency.", - "", - "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"marker\":{\"name\":\"LDL Cholesterol\",\"unit\":\"mg/dL\",\"referenceRange\":{\"lower\":0,\"upper\":130}},\"dataCoverage\":{\"totalReadings\":4,\"readingsInSnapshot\":4,\"newestTakenAt\":\"2026-05-26T09:15:00.000Z\",\"oldestInSnapshot\":\"2025-06-20T09:15:00.000Z\"},\"latest\":{\"value\":104,\"takenAt\":\"2026-05-26T09:15:00.000Z\",\"rangeStatus\":\"in-range\"},\"summary\":{\"points\":4,\"start\":142,\"end\":104,\"delta\":-38,\"mean\":123.75,\"min\":104,\"max\":142},\"series\":[{\"day\":\"2025-06-20\",\"value\":142},{\"day\":\"2025-10-08\",\"value\":131},{\"day\":\"2026-02-05\",\"value\":118},{\"day\":\"2026-05-26\",\"value\":104}]}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, "notes": [ { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "cached-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "forced-unchanged-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "next-day-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "read-only-stale": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z", - "preparing": false, - "revalidating": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "prose-de": { - "calls": [ - { - "system": [ - "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", - "", - "DEINE DATENGRUNDLAGE (graded snapshot):", - "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", - "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", - "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", - "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", - "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", - "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", - "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", - "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", - "", - "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", - "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", - "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", - "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", - "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", - "(d) einen Punkt zum Im-Auge-behalten;", - "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", - "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", - "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", - "", - "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", - "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", - "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", - "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", - "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", - "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", - "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", - "", - "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", - "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", - "", - "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", - "", - "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", - "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", - "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", - "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", - "", - "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", - "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", - "", - "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", - "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", - "", - "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", - "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", - "", - "AKUTE WARNZEICHEN (Vertrag, kein Stil)", - "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", - "", - "ERDUNG — NULL HALLUZINATIONEN", - "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", - "", - "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", - "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", - "", - "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", - "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", - "", - "EINORDNEN, NICHT AUFZÄHLEN", - "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", - "", - "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", - "", - "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", - "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", - "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", - "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", - "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", - "", - "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", - "{ \"summary\": \"...\" }", - "", - "METRIK — LABORWERT:", - "- ABWEICHENDE DATENFORM: Dieser Snapshot trägt KEINEN signal-Block. Die Felder der Basisanweisung, die auf signal verweisen, gelten hier nicht. Stattdessen: marker (name, unit, referenceRange), dataCoverage, latest (value, takenAt, rangeStatus) und series (aufsteigend, je { day, value }).", - "- Führe aus latest.rangeStatus — der vorab berechneten Einordnung gegen den Referenzbereich (in/unter/über dem Bereich, oder ohne Bereich) — und aus der Tendenz über series. Beides ist bereits bestimmt; nenne es, rechne es NICHT neu.", - "- Die eigene Historie führt: Wie der Wert gegenüber den letzten Abnahmen dieser Person steht, ist die Aussage. Der Referenzbereich ist ein grober sekundärer Anker, kein Urteil — ein Wert knapp außerhalb des Bereichs, der über Jahre stabil ist, ist etwas anderes als derselbe Wert nach einem klaren Sprung.", - "- Stabilität ist selbst ein Befund: Bewegt sich der Wert über die Abnahmen kaum, benenne die Konstanz — das ist bei einem Laborwert die häufigste und nützlichste Aussage.", - "- Einzelne Abnahmen schwanken (Labor, Tageszeit, Nüchternheit). Aus ZWEI Punkten keinen Trend bauen; ist die Historie kurz, ehrlich sagen, dass es für eine Tendenz noch zu wenig ist.", - "- KEINE Diagnose, keine Ursachenzuschreibung, keine Medikamenten- oder Supplement-Empfehlung. Bei deutlich auffälligen Werten neutral auf die ärztliche Einordnung verweisen — das ist der Abschluss, nicht ein erfundener Selbsthilfe-Schritt.", - "- Eine Botschaft: Schließe nur dann mit EINEM machbaren Schritt, wenn der Befund wirklich einen hergibt (z. B. den Wert bei der nächsten Kontrolle mitnehmen). Ist der Wert stabil und im Rahmen, erkenne das an, statt einen Schritt zu erzwingen.", - "- Der zu beurteilende Marker ist „LDL Cholesterol\"." - ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", - "Schreibe eine kurze Einschätzung zu diesem Laborwert. Beginne mit der EINORDNUNG in klaren Worten — wie der Marker steht, nicht der Zahl (z. B. \"hält sich stabil knapp innerhalb des Bereichs\", \"liegt einen Schritt über dem, wo er sonst läuft\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg; führe nie mit dem Wert. Ordne ihn zuerst gegen die eigenen letzten Abnahmen dieser Person ein, den Referenzbereich nur als groben Anker. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Anzahl und Aktualität der Abnahmen ableiten.", - "", - "{\"locale\":\"de\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"marker\":{\"name\":\"LDL Cholesterol\",\"unit\":\"mg/dL\",\"referenceRange\":{\"lower\":0,\"upper\":130}},\"dataCoverage\":{\"totalReadings\":4,\"readingsInSnapshot\":4,\"newestTakenAt\":\"2026-05-26T09:15:00.000Z\",\"oldestInSnapshot\":\"2025-06-20T09:15:00.000Z\"},\"latest\":{\"value\":104,\"takenAt\":\"2026-05-26T09:15:00.000Z\",\"rangeStatus\":\"in-range\"},\"summary\":{\"points\":4,\"start\":142,\"end\":104,\"delta\":-38,\"mean\":123.75,\"min\":104,\"max\":142},\"series\":[{\"day\":\"2025-06-20\",\"value\":142},{\"day\":\"2025-10-08\",\"value\":131},{\"day\":\"2026-02-05\",\"value\":118},{\"day\":\"2026-05-26\",\"value\":104}]}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Das ist kein JSON.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": null, + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "unparseable-fr": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — LAB MARKER:", - "- DIFFERENT DATA SHAPE: this snapshot carries NO signal block. The base instruction's fields that reference signal do not apply here. Instead: marker (name, unit, referenceRange), dataCoverage, latest (value, takenAt, rangeStatus) and series (ascending, each { day, value }).", - "- Lead from latest.rangeStatus — the pre-computed placement against the reference range (inside/below/above it, or no range on file) — and from the direction across series. Both are already determined; STATE them, do NOT recompute them.", - "- Their own history leads: how this value sits against this person's own recent draws is the message. The reference range is a coarse secondary anchor, not a verdict — a value slightly outside the band that has held steady for years is a different story from the same value after a clear step.", - "- Steadiness is itself a finding: when the value barely moves across draws, name that constancy — for a lab marker it is the most common and most useful thing to say.", - "- Single draws vary (lab, time of day, fasting state). Do not build a trend out of TWO points; when the history is short, say honestly that it is too little for a direction yet.", - "- NO diagnosis, no attribution of cause, no medication or supplement recommendation. For clearly notable values, refer neutrally to a clinician for interpretation — that is the close, not an invented self-help step.", - "- One message: close with ONE doable step only when the finding genuinely implies one (e.g. take the value to the next check-up). When it is steady and in range, acknowledge that rather than manufacturing a step.", - "- The marker under assessment is \"LDL Cholesterol\".", - "", - "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", - "Write one short assessment of this person's lab marker. Open with how the marker SITS in plain words — the read, not the number (e.g. \"holding steady just inside the band\", \"sitting a step above where it usually runs\") — then bring in ONE concrete figure from the snapshot right after as support; never lead with the value. Place it against this person's own recent draws first and the reference range only as a coarse anchor. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the number of draws and their recency.", - "", - "{\"locale\":\"fr\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"marker\":{\"name\":\"LDL Cholesterol\",\"unit\":\"mg/dL\",\"referenceRange\":{\"lower\":0,\"upper\":130}},\"dataCoverage\":{\"totalReadings\":4,\"readingsInSnapshot\":4,\"newestTakenAt\":\"2026-05-26T09:15:00.000Z\",\"oldestInSnapshot\":\"2025-06-20T09:15:00.000Z\"},\"latest\":{\"value\":104,\"takenAt\":\"2026-05-26T09:15:00.000Z\",\"rangeStatus\":\"in-range\"},\"summary\":{\"points\":4,\"start\":142,\"end\":104,\"delta\":-38,\"mean\":123.75,\"min\":104,\"max\":142},\"series\":[{\"day\":\"2025-06-20\",\"value\":142},{\"day\":\"2025-10-08\",\"value\":131},{\"day\":\"2026-02-05\",\"value\":118},{\"day\":\"2026-05-26\",\"value\":104}]}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Aucune évaluation pour « LDL Cholesterol » pour le moment. Une valeur de laboratoire se lit d’abord contre tes propres prélèvements précédents — un intervalle de référence est un repère grossier, pas un verdict. Une valeur qui a à peine bougé sur plusieurs prélèvements raconte autre chose qu’une valeur qui vient de faire un saut. Ton prochain contrôle médical est le bon endroit pour la remettre en contexte.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", + "items": null, "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": null, + "text": "Parity note for fresh-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": false, + "hasProvider": true, + "insufficient": true, + "text": null, + "updatedAt": null + } + }, + "bare-error-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "de", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "insufficient": true, + "text": null, + "updatedAt": null + } }, "blocked-es": { "calls": [ { + "maxTokens": 700, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's lab marker. Open with how the marker SITS in plain words — the read, not the number (e.g. \"holding steady just inside the band\", \"sitting a step above where it usually runs\") — then bring in ONE concrete figure from the snapshot right after as support; never lead with the value. Place it against this person's own recent draws first and the reference range only as a coarse anchor. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the number of draws and their recency.", + "", + "{\"locale\":\"es\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"marker\":{\"name\":\"LDL Cholesterol\",\"unit\":\"mg/dL\",\"referenceRange\":{\"lower\":0,\"upper\":130}},\"dataCoverage\":{\"totalReadings\":4,\"readingsInSnapshot\":4,\"newestTakenAt\":\"2026-05-26T09:15:00.000Z\",\"oldestInSnapshot\":\"2025-06-20T09:15:00.000Z\"},\"latest\":{\"value\":104,\"takenAt\":\"2026-05-26T09:15:00.000Z\",\"rangeStatus\":\"in-range\"},\"summary\":{\"points\":4,\"start\":142,\"end\":104,\"delta\":-38,\"mean\":123.75,\"min\":104,\"max\":142},\"series\":[{\"day\":\"2025-06-20\",\"value\":142},{\"day\":\"2025-10-08\",\"value\":131},{\"day\":\"2026-02-05\",\"value\":118},{\"day\":\"2026-05-26\",\"value\":104}]}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -651,537 +235,953 @@ "", "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "de", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Ahora mismo no hay ninguna valoración de «LDL Cholesterol». Un valor de laboratorio se lee primero frente a tus propias extracciones anteriores — un intervalo de referencia es un ancla aproximada, no un veredicto. Uno que apenas se ha movido a lo largo de varias extracciones cuenta algo distinto de uno que acaba de dar un salto. Tu próxima revisión médica es el lugar adecuado para ponerlo en contexto.", + "updatedAt": null + } + }, + "cached-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "foreign-marker": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "de", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "insufficient": true, + "text": null, + "updatedAt": null + } + }, + "fresh-en": { + "calls": [ + { + "maxTokens": 700, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", "Write one short assessment of this person's lab marker. Open with how the marker SITS in plain words — the read, not the number (e.g. \"holding steady just inside the band\", \"sitting a step above where it usually runs\") — then bring in ONE concrete figure from the snapshot right after as support; never lead with the value. Place it against this person's own recent draws first and the reference range only as a coarse anchor. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the number of draws and their recency.", "", - "{\"locale\":\"es\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"marker\":{\"name\":\"LDL Cholesterol\",\"unit\":\"mg/dL\",\"referenceRange\":{\"lower\":0,\"upper\":130}},\"dataCoverage\":{\"totalReadings\":4,\"readingsInSnapshot\":4,\"newestTakenAt\":\"2026-05-26T09:15:00.000Z\",\"oldestInSnapshot\":\"2025-06-20T09:15:00.000Z\"},\"latest\":{\"value\":104,\"takenAt\":\"2026-05-26T09:15:00.000Z\",\"rangeStatus\":\"in-range\"},\"summary\":{\"points\":4,\"start\":142,\"end\":104,\"delta\":-38,\"mean\":123.75,\"min\":104,\"max\":142},\"series\":[{\"day\":\"2025-06-20\",\"value\":142},{\"day\":\"2025-10-08\",\"value\":131},{\"day\":\"2026-02-05\",\"value\":118},{\"day\":\"2026-05-26\",\"value\":104}]}" - ] + "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"marker\":{\"name\":\"LDL Cholesterol\",\"unit\":\"mg/dL\",\"referenceRange\":{\"lower\":0,\"upper\":130}},\"dataCoverage\":{\"totalReadings\":4,\"readingsInSnapshot\":4,\"newestTakenAt\":\"2026-05-26T09:15:00.000Z\",\"oldestInSnapshot\":\"2025-06-20T09:15:00.000Z\"},\"latest\":{\"value\":104,\"takenAt\":\"2026-05-26T09:15:00.000Z\",\"rangeStatus\":\"in-range\"},\"summary\":{\"points\":4,\"start\":142,\"end\":104,\"delta\":-38,\"mean\":123.75,\"min\":104,\"max\":142},\"series\":[{\"day\":\"2025-06-20\",\"value\":142},{\"day\":\"2025-10-08\",\"value\":131},{\"day\":\"2026-02-05\",\"value\":118},{\"day\":\"2026-05-26\",\"value\":104}]}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — LAB MARKER:", + "- DIFFERENT DATA SHAPE: this snapshot carries NO signal block. The base instruction's fields that reference signal do not apply here. Instead: marker (name, unit, referenceRange), dataCoverage, latest (value, takenAt, rangeStatus) and series (ascending, each { day, value }).", + "- Lead from latest.rangeStatus — the pre-computed placement against the reference range (inside/below/above it, or no range on file) — and from the direction across series. Both are already determined; STATE them, do NOT recompute them.", + "- Their own history leads: how this value sits against this person's own recent draws is the message. The reference range is a coarse secondary anchor, not a verdict — a value slightly outside the band that has held steady for years is a different story from the same value after a clear step.", + "- Steadiness is itself a finding: when the value barely moves across draws, name that constancy — for a lab marker it is the most common and most useful thing to say.", + "- Single draws vary (lab, time of day, fasting state). Do not build a trend out of TWO points; when the history is short, say honestly that it is too little for a direction yet.", + "- NO diagnosis, no attribution of cause, no medication or supplement recommendation. For clearly notable values, refer neutrally to a clinician for interpretation — that is the close, not an invented self-help step.", + "- One message: close with ONE doable step only when the finding genuinely implies one (e.g. take the value to the next check-up). When it is steady and in range, acknowledge that rather than manufacturing a step.", + "- The marker under assessment is \"LDL Cholesterol\"." + ], + "temperature": 0.45, "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "Ahora mismo no hay ninguna valoración de «LDL Cholesterol». Un valor de laboratorio se lee primero frente a tus propias extracciones anteriores — un intervalo de referencia es un ancla aproximada, no un veredicto. Uno que apenas se ha movido a lo largo de varias extracciones cuenta algo distinto de uno que acaba de dar un salto. Tu próxima revisión médica es el lugar adecuado para ponerlo en contexto.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "text": "Parity note for fresh-en.", + "userId": "parity-rich" } - ] - }, - "bare-error-en": { - "calls": [], - "enqueued": [], + ], "result": { - "hasProvider": true, - "text": null, "cached": false, - "updatedAt": null, - "insufficient": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "bare-error-de": { - "calls": [], - "enqueued": [], - "result": { "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "insufficient": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "keyless-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment for \"LDL Cholesterol\" right now. A lab value reads first against your own previous draws — a reference range is a coarse anchor, not a verdict. One that has barely moved across several draws tells a different story from one that has just stepped. Your next check-up is the right place to put it in context.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } }, "keyless-de": { "calls": [], "enqueued": [], - "result": { - "hasProvider": false, - "text": "Für diesen Laborwert zu „LDL Cholesterol“ liegt gerade keine Einschätzung vor. Ein Laborwert liest sich zuerst gegen die eigenen vorherigen Abnahmen — ein Referenzbereich ist ein grober Anker, kein Urteil. Ein Wert, der sich über mehrere Abnahmen kaum bewegt, erzählt etwas anderes als einer, der gerade gesprungen ist. Die nächste ärztliche Kontrolle ist der richtige Ort, ihn einzuordnen.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "de", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": null, + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": null, + "text": "Parity note for fresh-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Für diesen Laborwert zu „LDL Cholesterol“ liegt gerade keine Einschätzung vor. Ein Laborwert liest sich zuerst gegen die eigenen vorherigen Abnahmen — ein Referenzbereich ist ein grober Anker, kein Urteil. Ein Wert, der sich über mehrere Abnahmen kaum bewegt, erzählt etwas anderes als einer, der gerade gesprungen ist. Die nächste ärztliche Kontrolle ist der richtige Ort, ihn einzuordnen.", + "updatedAt": null + } + }, + "keyless-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "de", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment for \"LDL Cholesterol\" right now. A lab value reads first against your own previous draws — a reference range is a coarse anchor, not a verdict. One that has barely moved across several draws tells a different story from one that has just stepped. Your next check-up is the right place to put it in context.", + "updatedAt": null + } }, "keyless-read-only": { "calls": [], "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment for \"LDL Cholesterol\" right now. A lab value reads first against your own previous draws — a reference range is a coarse anchor, not a verdict. One that has barely moved across several draws tells a different story from one that has just stepped. Your next check-up is the right place to put it in context.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "de", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": null, + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": null, + "text": "Parity note for fresh-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment for \"LDL Cholesterol\" right now. A lab value reads first against your own previous draws — a reference range is a coarse anchor, not a verdict. One that has barely moved across several draws tells a different story from one that has just stepped. Your next check-up is the right place to put it in context.", + "updatedAt": null + } + }, + "next-day-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } }, "no-readings": { "calls": [], "enqueued": [], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "insufficient": true - }, "notes": [ { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "de", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": null, + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": null, + "text": "Parity note for fresh-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": false, + "hasProvider": true, + "insufficient": true, + "text": null, + "updatedAt": null + } }, - "foreign-marker": { - "calls": [], + "prose-de": { + "calls": [ + { + "maxTokens": 700, + "messages": [ + { + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zu diesem Laborwert. Beginne mit der EINORDNUNG in klaren Worten — wie der Marker steht, nicht der Zahl (z. B. \"hält sich stabil knapp innerhalb des Bereichs\", \"liegt einen Schritt über dem, wo er sonst läuft\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg; führe nie mit dem Wert. Ordne ihn zuerst gegen die eigenen letzten Abnahmen dieser Person ein, den Referenzbereich nur als groben Anker. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Anzahl und Aktualität der Abnahmen ableiten.", + "", + "{\"locale\":\"de\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"marker\":{\"name\":\"LDL Cholesterol\",\"unit\":\"mg/dL\",\"referenceRange\":{\"lower\":0,\"upper\":130}},\"dataCoverage\":{\"totalReadings\":4,\"readingsInSnapshot\":4,\"newestTakenAt\":\"2026-05-26T09:15:00.000Z\",\"oldestInSnapshot\":\"2025-06-20T09:15:00.000Z\"},\"latest\":{\"value\":104,\"takenAt\":\"2026-05-26T09:15:00.000Z\",\"rangeStatus\":\"in-range\"},\"summary\":{\"points\":4,\"start\":142,\"end\":104,\"delta\":-38,\"mean\":123.75,\"min\":104,\"max\":142},\"series\":[{\"day\":\"2025-06-20\",\"value\":142},{\"day\":\"2025-10-08\",\"value\":131},{\"day\":\"2026-02-05\",\"value\":118},{\"day\":\"2026-05-26\",\"value\":104}]}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — LABORWERT:", + "- ABWEICHENDE DATENFORM: Dieser Snapshot trägt KEINEN signal-Block. Die Felder der Basisanweisung, die auf signal verweisen, gelten hier nicht. Stattdessen: marker (name, unit, referenceRange), dataCoverage, latest (value, takenAt, rangeStatus) und series (aufsteigend, je { day, value }).", + "- Führe aus latest.rangeStatus — der vorab berechneten Einordnung gegen den Referenzbereich (in/unter/über dem Bereich, oder ohne Bereich) — und aus der Tendenz über series. Beides ist bereits bestimmt; nenne es, rechne es NICHT neu.", + "- Die eigene Historie führt: Wie der Wert gegenüber den letzten Abnahmen dieser Person steht, ist die Aussage. Der Referenzbereich ist ein grober sekundärer Anker, kein Urteil — ein Wert knapp außerhalb des Bereichs, der über Jahre stabil ist, ist etwas anderes als derselbe Wert nach einem klaren Sprung.", + "- Stabilität ist selbst ein Befund: Bewegt sich der Wert über die Abnahmen kaum, benenne die Konstanz — das ist bei einem Laborwert die häufigste und nützlichste Aussage.", + "- Einzelne Abnahmen schwanken (Labor, Tageszeit, Nüchternheit). Aus ZWEI Punkten keinen Trend bauen; ist die Historie kurz, ehrlich sagen, dass es für eine Tendenz noch zu wenig ist.", + "- KEINE Diagnose, keine Ursachenzuschreibung, keine Medikamenten- oder Supplement-Empfehlung. Bei deutlich auffälligen Werten neutral auf die ärztliche Einordnung verweisen — das ist der Abschluss, nicht ein erfundener Selbsthilfe-Schritt.", + "- Eine Botschaft: Schließe nur dann mit EINEM machbaren Schritt, wenn der Befund wirklich einen hergibt (z. B. den Wert bei der nächsten Kontrolle mitnehmen). Ist der Wert stabil und im Rahmen, erkenne das an, statt einen Schritt zu erzwingen.", + "- Der zu beurteilende Marker ist „LDL Cholesterol\"." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], "enqueued": [], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "insufficient": true - }, "notes": [ { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "de", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": null, + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", - "snapshotHash": null, "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "biomarker:parity-rich-parity-ldl", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "text": "Parity note for fresh-en.", + "userId": "parity-rich" } - ] + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Das ist kein JSON.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "read-only-cold": { + "calls": [], + "enqueued": [ + { + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "userId": "parity-rich" + } + ], + "notes": [], + "result": { + "cached": false, + "hasProvider": true, + "preparing": true, + "revalidating": false, + "text": null, + "updatedAt": null + } + }, + "read-only-stale": { + "calls": [], + "enqueued": [ + { + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "userId": "parity-rich" + } + ], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "preparing": false, + "revalidating": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "unparseable-fr": { + "calls": [ + { + "maxTokens": 700, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's lab marker. Open with how the marker SITS in plain words — the read, not the number (e.g. \"holding steady just inside the band\", \"sitting a step above where it usually runs\") — then bring in ONE concrete figure from the snapshot right after as support; never lead with the value. Place it against this person's own recent draws first and the reference range only as a coarse anchor. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the number of draws and their recency.", + "", + "{\"locale\":\"fr\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"marker\":{\"name\":\"LDL Cholesterol\",\"unit\":\"mg/dL\",\"referenceRange\":{\"lower\":0,\"upper\":130}},\"dataCoverage\":{\"totalReadings\":4,\"readingsInSnapshot\":4,\"newestTakenAt\":\"2026-05-26T09:15:00.000Z\",\"oldestInSnapshot\":\"2025-06-20T09:15:00.000Z\"},\"latest\":{\"value\":104,\"takenAt\":\"2026-05-26T09:15:00.000Z\",\"rangeStatus\":\"in-range\"},\"summary\":{\"points\":4,\"start\":142,\"end\":104,\"delta\":-38,\"mean\":123.75,\"min\":104,\"max\":142},\"series\":[{\"day\":\"2025-06-20\",\"value\":142},{\"day\":\"2025-10-08\",\"value\":131},{\"day\":\"2026-02-05\",\"value\":118},{\"day\":\"2026-05-26\",\"value\":104}]}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — LAB MARKER:", + "- DIFFERENT DATA SHAPE: this snapshot carries NO signal block. The base instruction's fields that reference signal do not apply here. Instead: marker (name, unit, referenceRange), dataCoverage, latest (value, takenAt, rangeStatus) and series (ascending, each { day, value }).", + "- Lead from latest.rangeStatus — the pre-computed placement against the reference range (inside/below/above it, or no range on file) — and from the direction across series. Both are already determined; STATE them, do NOT recompute them.", + "- Their own history leads: how this value sits against this person's own recent draws is the message. The reference range is a coarse secondary anchor, not a verdict — a value slightly outside the band that has held steady for years is a different story from the same value after a clear step.", + "- Steadiness is itself a finding: when the value barely moves across draws, name that constancy — for a lab marker it is the most common and most useful thing to say.", + "- Single draws vary (lab, time of day, fasting state). Do not build a trend out of TWO points; when the history is short, say honestly that it is too little for a direction yet.", + "- NO diagnosis, no attribution of cause, no medication or supplement recommendation. For clearly notable values, refer neutrally to a clinician for interpretation — that is the close, not an invented self-help step.", + "- One message: close with ONE doable step only when the finding genuinely implies one (e.g. take the value to the next check-up). When it is steady and in range, acknowledge that rather than manufacturing a step.", + "- The marker under assessment is \"LDL Cholesterol\".", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "de", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "items": null, + "locale": "en", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": null, + "retryAt": null, + "snapshotHash": null, + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "biomarker:parity-rich-parity-ldl", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Aucune évaluation pour « LDL Cholesterol » pour le moment. Une valeur de laboratoire se lit d’abord contre tes propres prélèvements précédents — un intervalle de référence est un repère grossier, pas un verdict. Une valeur qui a à peine bougé sur plusieurs prélèvements raconte autre chose qu’une valeur qui vient de faire un saut. Ton prochain contrôle médical est le bon endroit pour la remettre en contexte.", + "updatedAt": null + } } } diff --git a/tests/integration/__golden__/status-parity/blood-pressure.json b/tests/integration/__golden__/status-parity/blood-pressure.json index 5d407df01..537fe2b70 100644 --- a/tests/integration/__golden__/status-parity/blood-pressure.json +++ b/tests/integration/__golden__/status-parity/blood-pressure.json @@ -1,408 +1,30 @@ { - "read-only-cold": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "preparing": true, - "revalidating": false - }, - "notes": [] - }, - "fresh-en": { + "bare-error-de": { "calls": [ { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — BLOOD PRESSURE:", - "- The snapshot carries systolic (bloodPressure.systolic) and diastolic (bloodPressure.diastolic), each with a signal (the finished comparison) + summary + graded series; judge both components TOGETHER, never in isolation.", - "- Trend over days/weeks: lead from bloodPressure.systolic.signal and bloodPressure.diastolic.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", - "- Guideline anchors (ESH 2023), for placement, not diagnosis:", - " * Optimal < 120/80, normal 120-129/80-84, high-normal 130-139/85-89.", - " * Hypertension grade 1 from 140/90, grade 2 from 160/100, grade 3 from 180/110.", - " * Common treatment target for uncomplicated hypertension < 130/80 (age 18-69), < 140/80 (age ≥ 70).", - "- targets: if bloodPressure.targets is present, place values against the personal target band; inTargetPctLast30DailyPoints is the share of recent days in target.", - "- Medication link: bpMedications carries compliance7/compliance30. Mention bpMedicationContinuityVsSystolic.correlation and weightVsSystolic.correlation only when present and |r| > 0.4 — as an association, never a cause.", - "- Mood: mention moodContext.moodVsSystolicCorrelation only when present and |r| > 0.4.", - "- One message: if the values sit above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly and name one thing worth keeping an eye on instead of manufacturing a step." - ], + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with the overall read in plain words, then bring in the number as support — not number-first.", - "Write one short assessment of this person's blood pressure. Open with what the reading MEANS in plain words — the overall read, not the number (e.g. \"calm and right where you want it\", \"running a touch higher than your usual this week\") — then bring in the systolic/diastolic figures right after as support, judged as ONE pair and placed against their own weekly/monthly baseline; never lead with the numbers. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zum Blutdruck dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Gesamteindruck, nicht der Zahl (z. B. \"ruhig und genau da, wo du ihn haben willst\", \"diese Woche einen Tick höher als sonst\") — und bring danach die systolischen/diastolischen Werte als Beleg, als EIN Paar beurteilt und gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit den Werten. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", "", "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the single most recent value and where it sits — only if the data genuinely supports that angle; never force it, never invent a trend.", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", "", - "DATA STRENGTH: 94 day-buckets in the window, latest reading today.", - "", - "REPETITION: you have already given a similar \"no material change\" assessment for this metric 11 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 12 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", - "", - "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", - "- Higher blood glucose looks like it goes with higher next-day systolic blood pressure in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=23, r=0.91)", - "- Higher systolic blood pressure looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=47, r=0.88)", - "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", "", "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"your systolic\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"your diastolic\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" - ] + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"bloodPressure\":{\"systolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"diastolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"paired\":{\"summary\":null,\"series\":[]},\"targets\":null},\"weightVsSystolic\":{\"correlation\":null,\"pairs\":[]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":0,\"medicationCount\":0,\"correlation\":null,\"series\":[]},\"bpMedications\":[],\"moodContext\":null}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "cached-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "forced-unchanged-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "next-day-en": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — BLOOD PRESSURE:", - "- The snapshot carries systolic (bloodPressure.systolic) and diastolic (bloodPressure.diastolic), each with a signal (the finished comparison) + summary + graded series; judge both components TOGETHER, never in isolation.", - "- Trend over days/weeks: lead from bloodPressure.systolic.signal and bloodPressure.diastolic.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", - "- Guideline anchors (ESH 2023), for placement, not diagnosis:", - " * Optimal < 120/80, normal 120-129/80-84, high-normal 130-139/85-89.", - " * Hypertension grade 1 from 140/90, grade 2 from 160/100, grade 3 from 180/110.", - " * Common treatment target for uncomplicated hypertension < 130/80 (age 18-69), < 140/80 (age ≥ 70).", - "- targets: if bloodPressure.targets is present, place values against the personal target band; inTargetPctLast30DailyPoints is the share of recent days in target.", - "- Medication link: bpMedications carries compliance7/compliance30. Mention bpMedicationContinuityVsSystolic.correlation and weightVsSystolic.correlation only when present and |r| > 0.4 — as an association, never a cause.", - "- Mood: mention moodContext.moodVsSystolicCorrelation only when present and |r| > 0.4.", - "- One message: if the values sit above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly and name one thing worth keeping an eye on instead of manufacturing a step." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-16", - "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", - "Write one short assessment of this person's blood pressure. Open with what the reading MEANS in plain words — the overall read, not the number (e.g. \"calm and right where you want it\", \"running a touch higher than your usual this week\") — then bring in the systolic/diastolic figures right after as support, judged as ONE pair and placed against their own weekly/monthly baseline; never lead with the numbers. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", - "Parity note for fresh-en.", - "", - "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the single most recent value and where it sits — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 94 day-buckets in the window, latest reading 1 day ago.", - "", - "REPETITION: you have already given a similar \"no material change\" assessment for this metric 11 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 12 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", - "", - "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", - "- Higher blood glucose looks like it goes with higher next-day systolic blood pressure in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=23, r=0.91)", - "- Higher systolic blood pressure looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=47, r=0.88)", - "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":1},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"your systolic\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.66,\"baselineLabel\":\"your monthly average\",\"delta\":0.63,\"deltaPct\":0.5,\"spread\":2.2,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":1,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":118,\"max\":121,\"mean\":119.33,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":117,\"max\":133,\"mean\":127.25,\"n\":12}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"your diastolic\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.34,\"baselineLabel\":\"your monthly average\",\"delta\":0.37,\"deltaPct\":0.5,\"spread\":1.89,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":1,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":78,\"mean\":76.33,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":81.75,\"n\":12}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":98,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":100,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":101,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":103,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":104,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":106,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":107,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":109,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":110,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":112,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":113,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":115,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":116,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":118,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":119,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":121,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":122,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":125,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":127,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":128,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":131,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":133,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":134,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":136,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":137,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":139,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":140,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":50,\"compliance30\":76}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":2,\"max\":5,\"mean\":3.5,\"n\":2}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 250, "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for next-day-en.", - "cached": false, - "updatedAt": "2026-06-16T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "read-only-stale": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": "Parity note for next-day-en.", - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z", - "preparing": false, - "revalidating": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "prose-de": { - "calls": [ - { "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -491,79 +113,124 @@ "- Stimmung: moodContext.moodVsSystolicCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4.", "- Eine Botschaft: Wenn die Werte über der Baseline liegen, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage zur selben Uhrzeit nachmessen, oder — bei lückenhafter Einnahmetreue — die Einnahme verlässlicher machen). Bei stabilen Werten im Ziel: das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne mit dem Gesamteindruck in klaren Worten und bring die Zahl danach als Beleg — nicht zahlen-zuerst.", - "Schreibe eine kurze Einschätzung zum Blutdruck dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Gesamteindruck, nicht der Zahl (z. B. \"ruhig und genau da, wo du ihn haben willst\", \"diese Woche einen Tick höher als sonst\") — und bring danach die systolischen/diastolischen Werte als Beleg, als EIN Paar beurteilt und gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit den Werten. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 94 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", - "", - "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Wenn der Blutzucker höher liegt, scheint der systolische Blutdruck am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=23, r=0.91)", - "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", - "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", - "", - "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"deine Systole\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"deine Diastole\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": true, - "text": "Das ist kein JSON.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, "notes": [ { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "blood-pressure", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" }, { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": null, "inputHash": null, - "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "blood-pressure", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "blood-pressure", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Für diese Karte liegt gerade keine Einschätzung vor. Blutdruck wird lesbar über eine Reihe ruhiger Messungen unter vergleichbaren Bedingungen — die Richtung über mehrere Tage sagt weit mehr als ein einzelner Ausreißer, und systolisch und diastolisch gehören dabei zusammen gelesen.", + "updatedAt": null + } }, - "unparseable-fr": { + "bare-error-en": { "calls": [ { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's blood pressure. Open with what the reading MEANS in plain words — the overall read, not the number (e.g. \"calm and right where you want it\", \"running a touch higher than your usual this week\") — then bring in the systolic/diastolic figures right after as support, judged as ONE pair and placed against their own weekly/monthly baseline; never lead with the numbers. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"bloodPressure\":{\"systolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"diastolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"paired\":{\"summary\":null,\"series\":[]},\"targets\":null},\"weightVsSystolic\":{\"correlation\":null,\"pairs\":[]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":0,\"medicationCount\":0,\"correlation\":null,\"series\":[]},\"bpMedications\":[],\"moodContext\":null}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -637,7 +304,7 @@ "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", "{ \"summary\": \"...\" }", "", "METRIC — BLOOD PRESSURE:", @@ -650,13 +317,93 @@ "- targets: if bloodPressure.targets is present, place values against the personal target band; inTargetPctLast30DailyPoints is the share of recent days in target.", "- Medication link: bpMedications carries compliance7/compliance30. Mention bpMedicationContinuityVsSystolic.correlation and weightVsSystolic.correlation only when present and |r| > 0.4 — as an association, never a cause.", "- Mood: mention moodContext.moodVsSystolicCorrelation only when present and |r| > 0.4.", - "- One message: if the values sit above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly and name one thing worth keeping an eye on instead of manufacturing a step.", - "", - "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + "- One message: if the values sit above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly and name one thing worth keeping an eye on instead of manufacturing a step." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "blood-pressure", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "blood-pressure", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. Blood pressure becomes readable over a run of calm readings under similar conditions — the direction across several days says far more than any single outlier, and systolic and diastolic are read together rather than one at a time.", + "updatedAt": null + } + }, + "blocked-es": { + "calls": [ + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open with the overall read in plain words, then bring in the number as support — not number-first.", @@ -672,74 +419,18 @@ "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", "", "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Quand la glycémie monte, la pression systolique du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=23, r=0.91)", - "- Quand la pression systolique monte, le poids du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=47, r=0.88)", + "- Cuando la glucosa en sangre es mayor, la presión sistólica del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=23, r=0.91)", + "- Cuando la presión sistólica es mayor, el peso del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=47, r=0.88)", "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", "", "", - "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"deine Systole\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"deine Diastole\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" - ] + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"deine Systole\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"deine Diastole\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Stable, comme d’habitude. Ta tension artérielle est à 127 en ce moment — au-dessus de ta moyenne habituelle de 127. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "blocked-es": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -830,9 +521,128 @@ "", "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "blood-pressure", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "blood-pressure", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Estable, como de costumbre. Tu presión arterial está en 127 ahora mismo — por encima de tu promedio habitual de 127. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", + "updatedAt": null + } + }, + "cached-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "fresh-en": { + "calls": [ + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open with the overall read in plain words, then bring in the number as support — not number-first.", @@ -841,94 +651,25 @@ "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", "", "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the single most recent value and where it sits — only if the data genuinely supports that angle; never force it, never invent a trend.", "", - "DATENLAGE: 94 Tages-Buckets im Fenster, letzter Wert heute.", + "DATA STRENGTH: 94 day-buckets in the window, latest reading today.", "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 11 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 12 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", "", - "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Cuando la glucosa en sangre es mayor, la presión sistólica del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=23, r=0.91)", - "- Cuando la presión sistólica es mayor, el peso del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=47, r=0.88)", - "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher blood glucose looks like it goes with higher next-day systolic blood pressure in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=23, r=0.91)", + "- Higher systolic blood pressure looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=47, r=0.88)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", "", "", - "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"deine Systole\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"deine Diastole\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" - ] + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"your systolic\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"your diastolic\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Estable, como de costumbre. Tu presión arterial está en 127 ahora mismo — por encima de tu promedio habitual de 127. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "bare-error-en": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -1017,111 +758,487 @@ "- Mood: mention moodContext.moodVsSystolicCorrelation only when present and |r| > 0.4.", "- One message: if the values sit above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly and name one thing worth keeping an eye on instead of manufacturing a step." ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with what this means for the user today, then the number behind it.", - "Write one short assessment of this person's blood pressure. Open with what the reading MEANS in plain words — the overall read, not the number (e.g. \"calm and right where you want it\", \"running a touch higher than your usual this week\") — then bring in the systolic/diastolic figures right after as support, judged as ONE pair and placed against their own weekly/monthly baseline; never lead with the numbers. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"bloodPressure\":{\"systolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"diastolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"paired\":{\"summary\":null,\"series\":[]},\"targets\":null},\"weightVsSystolic\":{\"correlation\":null,\"pairs\":[]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":0,\"medicationCount\":0,\"correlation\":null,\"series\":[]},\"bpMedications\":[],\"moodContext\":null}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. Blood pressure becomes readable over a run of calm readings under similar conditions — the direction across several days says far more than any single outlier, and systolic and diastolic are read together rather than one at a time.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "blood-pressure", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "blood-pressure", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" }, { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": null, "inputHash": null, - "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "blood-pressure", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "blood-pressure", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "blood-pressure", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Stabil und wie gewohnt. Dein Blutdruck liegt aktuell bei 127 — höher als dein üblicher Schnitt von 127. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", + "updatedAt": null + } }, - "bare-error-de": { + "keyless-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "blood-pressure", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "blood-pressure", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "blood-pressure", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Steady and much as usual. Your blood pressure is at 127 right now — higher than your usual average of 127. That sits in your usual range — nothing you need to act on right now.", + "updatedAt": null + } + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "blood-pressure", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "blood-pressure", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "blood-pressure", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. Blood pressure becomes readable over a run of calm readings under similar conditions — the direction across several days says far more than any single outlier, and systolic and diastolic are read together rather than one at a time.", + "updatedAt": null + } + }, + "next-day-en": { "calls": [ { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's blood pressure. Open with what the reading MEANS in plain words — the overall read, not the number (e.g. \"calm and right where you want it\", \"running a touch higher than your usual this week\") — then bring in the systolic/diastolic figures right after as support, judged as ONE pair and placed against their own weekly/monthly baseline; never lead with the numbers. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the single most recent value and where it sits — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 94 day-buckets in the window, latest reading 1 day ago.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 11 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 12 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher blood glucose looks like it goes with higher next-day systolic blood pressure in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=23, r=0.91)", + "- Higher systolic blood pressure looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=47, r=0.88)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":1},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"your systolic\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.66,\"baselineLabel\":\"your monthly average\",\"delta\":0.63,\"deltaPct\":0.5,\"spread\":2.2,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":1,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":118,\"max\":121,\"mean\":119.33,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":117,\"max\":133,\"mean\":127.25,\"n\":12}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"your diastolic\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.34,\"baselineLabel\":\"your monthly average\",\"delta\":0.37,\"deltaPct\":0.5,\"spread\":1.89,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":1,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":78,\"mean\":76.33,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":81.75,\"n\":12}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":98,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":100,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":101,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":103,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":104,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":106,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":107,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":109,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":110,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":112,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":113,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":115,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":116,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":118,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":119,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":121,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":122,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":125,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":127,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":128,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":131,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":133,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":134,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":136,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":137,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":139,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":140,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":50,\"compliance30\":76}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":2,\"max\":5,\"mean\":3.5,\"n\":2}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — BLOOD PRESSURE:", + "- The snapshot carries systolic (bloodPressure.systolic) and diastolic (bloodPressure.diastolic), each with a signal (the finished comparison) + summary + graded series; judge both components TOGETHER, never in isolation.", + "- Trend over days/weeks: lead from bloodPressure.systolic.signal and bloodPressure.diastolic.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Guideline anchors (ESH 2023), for placement, not diagnosis:", + " * Optimal < 120/80, normal 120-129/80-84, high-normal 130-139/85-89.", + " * Hypertension grade 1 from 140/90, grade 2 from 160/100, grade 3 from 180/110.", + " * Common treatment target for uncomplicated hypertension < 130/80 (age 18-69), < 140/80 (age ≥ 70).", + "- targets: if bloodPressure.targets is present, place values against the personal target band; inTargetPctLast30DailyPoints is the share of recent days in target.", + "- Medication link: bpMedications carries compliance7/compliance30. Mention bpMedicationContinuityVsSystolic.correlation and weightVsSystolic.correlation only when present and |r| > 0.4 — as an association, never a cause.", + "- Mood: mention moodContext.moodVsSystolicCorrelation only when present and |r| > 0.4.", + "- One message: if the values sit above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "prose-de": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit dem Gesamteindruck in klaren Worten und bring die Zahl danach als Beleg — nicht zahlen-zuerst.", + "Schreibe eine kurze Einschätzung zum Blutdruck dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Gesamteindruck, nicht der Zahl (z. B. \"ruhig und genau da, wo du ihn haben willst\", \"diese Woche einen Tick höher als sonst\") — und bring danach die systolischen/diastolischen Werte als Beleg, als EIN Paar beurteilt und gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit den Werten. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 94 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der Blutzucker höher liegt, scheint der systolische Blutdruck am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=23, r=0.91)", + "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"deine Systole\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"deine Diastole\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -1210,389 +1327,272 @@ "- Stimmung: moodContext.moodVsSystolicCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4.", "- Eine Botschaft: Wenn die Werte über der Baseline liegen, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage zur selben Uhrzeit nachmessen, oder — bei lückenhafter Einnahmetreue — die Einnahme verlässlicher machen). Bei stabilen Werten im Ziel: das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", - "Schreibe eine kurze Einschätzung zum Blutdruck dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Gesamteindruck, nicht der Zahl (z. B. \"ruhig und genau da, wo du ihn haben willst\", \"diese Woche einen Tick höher als sonst\") — und bring danach die systolischen/diastolischen Werte als Beleg, als EIN Paar beurteilt und gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit den Werten. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", - "", - "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"bloodPressure\":{\"systolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"diastolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"paired\":{\"summary\":null,\"series\":[]},\"targets\":null},\"weightVsSystolic\":{\"correlation\":null,\"pairs\":[]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":0,\"medicationCount\":0,\"correlation\":null,\"series\":[]},\"bpMedications\":[],\"moodContext\":null}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "Für diese Karte liegt gerade keine Einschätzung vor. Blutdruck wird lesbar über eine Reihe ruhiger Messungen unter vergleichbaren Bedingungen — die Richtung über mehrere Tage sagt weit mehr als ein einzelner Ausreißer, und systolisch und diastolisch gehören dabei zusammen gelesen.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "blood-pressure", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "blood-pressure", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "blood-pressure", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" } - ] + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Das ist kein JSON.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } }, - "keyless-en": { + "read-only-cold": { "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Steady and much as usual. Your blood pressure is at 127 right now — higher than your usual average of 127. That sits in your usual range — nothing you need to act on right now.", - "cached": true, - "updatedAt": null - }, - "notes": [ + "enqueued": [ { - "userId": "parity-bare", - "metric": "blood-pressure", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "blood-pressure", "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", "metric": "blood-pressure", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "userId": "parity-rich" } - ] + ], + "notes": [], + "result": { + "cached": false, + "hasProvider": true, + "preparing": true, + "revalidating": false, + "text": null, + "updatedAt": null + } }, - "keyless-de": { + "read-only-stale": { "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Stabil und wie gewohnt. Dein Blutdruck liegt aktuell bei 127 — höher als dein üblicher Schnitt von 127. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", - "cached": true, - "updatedAt": null - }, + "enqueued": [ + { + "locale": "en", + "metric": "blood-pressure", + "userId": "parity-rich" + } + ], "notes": [ { - "userId": "parity-bare", - "metric": "blood-pressure", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "blood-pressure", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": true, + "preparing": false, + "revalidating": true, + "text": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } }, - "keyless-read-only": { - "calls": [], + "unparseable-fr": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the overall read in plain words, then bring in the number as support — not number-first.", + "Write one short assessment of this person's blood pressure. Open with what the reading MEANS in plain words — the overall read, not the number (e.g. \"calm and right where you want it\", \"running a touch higher than your usual this week\") — then bring in the systolic/diastolic figures right after as support, judged as ONE pair and placed against their own weekly/monthly baseline; never lead with the numbers. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 94 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Quand la glycémie monte, la pression systolique du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=23, r=0.91)", + "- Quand la pression systolique monte, le poids du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=47, r=0.88)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"deine Systole\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"deine Diastole\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — BLOOD PRESSURE:", + "- The snapshot carries systolic (bloodPressure.systolic) and diastolic (bloodPressure.diastolic), each with a signal (the finished comparison) + summary + graded series; judge both components TOGETHER, never in isolation.", + "- Trend over days/weeks: lead from bloodPressure.systolic.signal and bloodPressure.diastolic.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Guideline anchors (ESH 2023), for placement, not diagnosis:", + " * Optimal < 120/80, normal 120-129/80-84, high-normal 130-139/85-89.", + " * Hypertension grade 1 from 140/90, grade 2 from 160/100, grade 3 from 180/110.", + " * Common treatment target for uncomplicated hypertension < 130/80 (age 18-69), < 140/80 (age ≥ 70).", + "- targets: if bloodPressure.targets is present, place values against the personal target band; inTargetPctLast30DailyPoints is the share of recent days in target.", + "- Medication link: bpMedications carries compliance7/compliance30. Mention bpMedicationContinuityVsSystolic.correlation and weightVsSystolic.correlation only when present and |r| > 0.4 — as an association, never a cause.", + "- Mood: mention moodContext.moodVsSystolicCorrelation only when present and |r| > 0.4.", + "- One message: if the values sit above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. Blood pressure becomes readable over a run of calm readings under similar conditions — the direction across several days says far more than any single outlier, and systolic and diastolic are read together rather than one at a time.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "blood-pressure", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "blood-pressure", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "blood-pressure", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "blood-pressure", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "blood-pressure", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "blood-pressure", + "inputHash": null, + "items": null, "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "blood-pressure", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Stable, comme d’habitude. Ta tension artérielle est à 127 en ce moment — au-dessus de ta moyenne habituelle de 127. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", + "updatedAt": null + } } } diff --git a/tests/integration/__golden__/status-parity/bmi.json b/tests/integration/__golden__/status-parity/bmi.json index 8c3705c05..84f41a1fd 100644 --- a/tests/integration/__golden__/status-parity/bmi.json +++ b/tests/integration/__golden__/status-parity/bmi.json @@ -1,592 +1,160 @@ { - "read-only-cold": { + "bare-error-de": { "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "preparing": true, - "revalidating": false - }, - "notes": [] - }, - "fresh-en": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — BMI:", - "- The snapshot carries bmi.signal (the finished comparison) + bmi.summary + bmi.series (graded). bmi.latestDayFocus shows the latest value, its WHO classification and the step from the prior day; bmi.target is the green band (18.5-24.9).", - "- This card's focus is WHO-BAND placement, not weight pace (the weight card carries that). Centre it on which band the value sits in and whether it crosses a band or nears a band boundary.", - "- WHO bands for rough placement, never as a label for the person: Underweight < 18.5, Normal 18.5-24.9, Overweight 25.0-29.9, Obesity I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. From age 65+, 22-27 counts as favourable (DEGAM).", - "- Band reference: lead from bmi.signal — current, delta vs baselineLabel and placement (band position) are already computed; STATE them, do NOT recompute. Call it out explicitly when the recent value has crossed a WHO band or is nearing a band boundary (within ~0.5 BMI points). When the value stays safely inside the same band, say so briefly — do not force a pace narrative.", - "- Name BMI's limits plainly: it does not measure body composition and is weakly informative for muscular or very athletic people.", - "- One message: close with ONE doable step that fits the band placement ONLY when the finding implies one — when the value is stable in the favourable band, acknowledge that honestly and name one thing worth keeping an eye on rather than forcing a finding or a step." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with what this means for the user today, then the number behind it.", - "Write one short assessment of this person's BMI. Open with what its band placement MEANS in plain words — where it sits and whether that's holding or shifting, not the number (e.g. \"sitting comfortably in the healthy band\", \"edging toward the next band up\") — then bring in the current value and WHO band right after as support, saying against their own weekly/monthly baseline whether it has crossed a band or is nearing a boundary; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Leave the weight pace to the weight card. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", - "", - "REPETITION: you have already given a similar \"no material change\" assessment for this metric 10 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 11 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"your BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, "notes": [ { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "cached-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "forced-unchanged-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Parity note for fresh-en.", "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "next-day-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "read-only-stale": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z", - "preparing": false, - "revalidating": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "prose-de": { - "calls": [ - { - "system": [ - "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", - "", - "DEINE DATENGRUNDLAGE (graded snapshot):", - "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", - "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", - "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", - "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", - "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", - "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", - "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", - "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", - "", - "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", - "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", - "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", - "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", - "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", - "(d) einen Punkt zum Im-Auge-behalten;", - "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", - "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", - "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", - "", - "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", - "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", - "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", - "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", - "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", - "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", - "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", - "", - "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", - "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", - "", - "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", - "", - "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", - "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", - "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", - "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", - "", - "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", - "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", - "", - "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", - "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", - "", - "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", - "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", - "", - "AKUTE WARNZEICHEN (Vertrag, kein Stil)", - "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", - "", - "ERDUNG — NULL HALLUZINATIONEN", - "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", - "", - "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", - "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", - "", - "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", - "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", - "", - "EINORDNEN, NICHT AUFZÄHLEN", - "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", - "", - "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", - "", - "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", - "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", - "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", - "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", - "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", - "", - "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", - "{ \"summary\": \"...\" }", - "", - "METRIK — BMI:", - "- Der Snapshot trägt bmi.signal (der fertige Vergleich) + bmi.summary + bmi.series (graded). bmi.latestDayFocus zeigt den jüngsten Wert, dessen WHO-Klassifikation und den Schritt zum Vortag; bmi.target ist das grüne Band (18.5-24.9).", - "- Fokus dieser Karte ist die WHO-BAND-EINORDNUNG, nicht das Gewichtstempo (das trägt die Gewichts-Karte). Setze den Schwerpunkt darauf, in welchem Band der Wert liegt und ob er ein Band wechselt oder sich einer Bandgrenze nähert.", - "- WHO-Bänder zur groben Einordnung, nie als Etikett für die Person: Untergewicht < 18.5, Normal 18.5-24.9, Übergewicht 25.0-29.9, Adipositas I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. Ab 65+ gilt 22-27 als günstig (DEGAM).", - "- Bandbezug: Führe mit bmi.signal — current, delta gegenüber baselineLabel und placement (Bandlage) sind bereits berechnet; NENNE sie, rechne sie NICHT neu. Nenne es ausdrücklich, wenn der recent-Wert ein WHO-Band gewechselt hat oder sich (innerhalb ~0.5 BMI-Punkte) einer Bandgrenze nähert. Bleibt der Wert sicher im selben Band, sage das knapp — keine Tempo-Erzählung erzwingen.", - "- Grenzen des BMI offen ansprechen: Er misst keine Körperzusammensetzung und ist bei muskulösen oder sehr sportlichen Personen wenig aussagekräftig.", - "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, der zur Bandlage passt, wenn der Befund einen nahelegt — bei stabilem Wert im günstigen Band das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." - ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", - "Schreibe eine kurze Einschätzung zum BMI dieser Person. Beginne mit der BEDEUTUNG der Bandlage in klaren Worten — wo er liegt und ob das hält oder sich verschiebt, nicht der Zahl (z. B. \"liegt bequem im gesunden Band\", \"nähert sich dem nächsten Band\") — und bring danach den aktuellen Wert und das WHO-Band als Beleg, samt Angabe gegen die eigene Wochen-/Monats-Baseline, ob er ein Band gewechselt hat oder sich einer Bandgrenze nähert; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Das Gewichtstempo bleibt der Gewichts-Karte überlassen. Konfidenz aus Messanzahl und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", - "", - "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"dein BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Das ist kein JSON.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "bmi", "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", + "metric": "bmi", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "unparseable-fr": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — BMI:", - "- The snapshot carries bmi.signal (the finished comparison) + bmi.summary + bmi.series (graded). bmi.latestDayFocus shows the latest value, its WHO classification and the step from the prior day; bmi.target is the green band (18.5-24.9).", - "- This card's focus is WHO-BAND placement, not weight pace (the weight card carries that). Centre it on which band the value sits in and whether it crosses a band or nears a band boundary.", - "- WHO bands for rough placement, never as a label for the person: Underweight < 18.5, Normal 18.5-24.9, Overweight 25.0-29.9, Obesity I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. From age 65+, 22-27 counts as favourable (DEGAM).", - "- Band reference: lead from bmi.signal — current, delta vs baselineLabel and placement (band position) are already computed; STATE them, do NOT recompute. Call it out explicitly when the recent value has crossed a WHO band or is nearing a band boundary (within ~0.5 BMI points). When the value stays safely inside the same band, say so briefly — do not force a pace narrative.", - "- Name BMI's limits plainly: it does not measure body composition and is weakly informative for muscular or very athletic people.", - "- One message: close with ONE doable step that fits the band placement ONLY when the finding implies one — when the value is stable in the favourable band, acknowledge that honestly and name one thing worth keeping an eye on rather than forcing a finding or a step.", - "", - "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with what this means for the user today, then the number behind it.", - "Write one short assessment of this person's BMI. Open with what its band placement MEANS in plain words — where it sits and whether that's holding or shifting, not the number (e.g. \"sitting comfortably in the healthy band\", \"edging toward the next band up\") — then bring in the current value and WHO band right after as support, saying against their own weekly/monthly baseline whether it has crossed a band or is nearing a boundary; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Leave the weight pace to the weight card. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", - "", - "", - "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"dein BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Un peu en dehors de tes repères habituels ces derniers temps. Ton IMC est à 26,6 en ce moment — au-dessus de ta moyenne habituelle de 26,1. Lis-le avec la tendance de ton poids et ta masse grasse sur quelques semaines plutôt qu’au jour le jour.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", + "items": null, "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", + "metric": "bmi", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "bmi", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "bmi", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "bmi", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Für die BMI-Einschätzung fehlen aktuell Größenangaben im Profil.", + "updatedAt": null + } + }, + "bare-error-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "de", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "bmi", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "bmi", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "BMI assessment currently requires height data in the profile.", + "updatedAt": null + } }, "blocked-es": { "calls": [ { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's BMI. Open with what its band placement MEANS in plain words — where it sits and whether that's holding or shifting, not the number (e.g. \"sitting comfortably in the healthy band\", \"edging toward the next band up\") — then bring in the current value and WHO band right after as support, saying against their own weekly/monthly baseline whether it has crossed a band or is nearing a boundary; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Leave the weight pace to the weight card. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "", + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"dein BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -673,9 +241,688 @@ "", "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "de", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "bmi", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "bmi", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Últimamente algo fuera de tu rango habitual. Tu IMC está en 26,6 ahora mismo — por encima de tu promedio habitual de 26,1. Léelo junto con la tendencia de tu peso y tu grasa corporal a lo largo de unas semanas, no día a día.", + "updatedAt": null + } + }, + "cached-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "fresh-en": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's BMI. Open with what its band placement MEANS in plain words — where it sits and whether that's holding or shifting, not the number (e.g. \"sitting comfortably in the healthy band\", \"edging toward the next band up\") — then bring in the current value and WHO band right after as support, saying against their own weekly/monthly baseline whether it has crossed a band or is nearing a boundary; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Leave the weight pace to the weight card. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 10 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 11 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"your BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — BMI:", + "- The snapshot carries bmi.signal (the finished comparison) + bmi.summary + bmi.series (graded). bmi.latestDayFocus shows the latest value, its WHO classification and the step from the prior day; bmi.target is the green band (18.5-24.9).", + "- This card's focus is WHO-BAND placement, not weight pace (the weight card carries that). Centre it on which band the value sits in and whether it crosses a band or nears a band boundary.", + "- WHO bands for rough placement, never as a label for the person: Underweight < 18.5, Normal 18.5-24.9, Overweight 25.0-29.9, Obesity I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. From age 65+, 22-27 counts as favourable (DEGAM).", + "- Band reference: lead from bmi.signal — current, delta vs baselineLabel and placement (band position) are already computed; STATE them, do NOT recompute. Call it out explicitly when the recent value has crossed a WHO band or is nearing a band boundary (within ~0.5 BMI points). When the value stays safely inside the same band, say so briefly — do not force a pace narrative.", + "- Name BMI's limits plainly: it does not measure body composition and is weakly informative for muscular or very athletic people.", + "- One message: close with ONE doable step that fits the band placement ONLY when the finding implies one — when the value is stable in the favourable band, acknowledge that honestly and name one thing worth keeping an eye on rather than forcing a finding or a step." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "de", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "bmi", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "bmi", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Zuletzt etwas außerhalb deines üblichen Rahmens. Dein BMI liegt aktuell bei 30,9 — höher als dein üblicher Schnitt von 30,3. Schau ihn dir zusammen mit Gewichtstrend und Körperfett über ein paar Wochen an.", + "updatedAt": null + } + }, + "keyless-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "de", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "bmi", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "bmi", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "A little off your usual lately. Your BMI is at 30.9 right now — higher than your usual average of 30.3. Read it alongside your weight trend and body-fat over a few weeks rather than day by day.", + "updatedAt": null + } + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "de", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "bmi", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "bmi", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. BMI is a rough orientation rather than a verdict, and it means most read alongside your weight trend and body composition. Sustained movement over weeks carries where a single value does not.", + "updatedAt": null + } + }, + "next-day-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "prose-de": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zum BMI dieser Person. Beginne mit der BEDEUTUNG der Bandlage in klaren Worten — wo er liegt und ob das hält oder sich verschiebt, nicht der Zahl (z. B. \"liegt bequem im gesunden Band\", \"nähert sich dem nächsten Band\") — und bring danach den aktuellen Wert und das WHO-Band als Beleg, samt Angabe gegen die eigene Wochen-/Monats-Baseline, ob er ein Band gewechselt hat oder sich einer Bandgrenze nähert; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Das Gewichtstempo bleibt der Gewichts-Karte überlassen. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"dein BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — BMI:", + "- Der Snapshot trägt bmi.signal (der fertige Vergleich) + bmi.summary + bmi.series (graded). bmi.latestDayFocus zeigt den jüngsten Wert, dessen WHO-Klassifikation und den Schritt zum Vortag; bmi.target ist das grüne Band (18.5-24.9).", + "- Fokus dieser Karte ist die WHO-BAND-EINORDNUNG, nicht das Gewichtstempo (das trägt die Gewichts-Karte). Setze den Schwerpunkt darauf, in welchem Band der Wert liegt und ob er ein Band wechselt oder sich einer Bandgrenze nähert.", + "- WHO-Bänder zur groben Einordnung, nie als Etikett für die Person: Untergewicht < 18.5, Normal 18.5-24.9, Übergewicht 25.0-29.9, Adipositas I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. Ab 65+ gilt 22-27 als günstig (DEGAM).", + "- Bandbezug: Führe mit bmi.signal — current, delta gegenüber baselineLabel und placement (Bandlage) sind bereits berechnet; NENNE sie, rechne sie NICHT neu. Nenne es ausdrücklich, wenn der recent-Wert ein WHO-Band gewechselt hat oder sich (innerhalb ~0.5 BMI-Punkte) einer Bandgrenze nähert. Bleibt der Wert sicher im selben Band, sage das knapp — keine Tempo-Erzählung erzwingen.", + "- Grenzen des BMI offen ansprechen: Er misst keine Körperzusammensetzung und ist bei muskulösen oder sehr sportlichen Personen wenig aussagekräftig.", + "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, der zur Bandlage passt, wenn der Befund einen nahelegt — bei stabilem Wert im günstigen Band das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "de", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Das ist kein JSON.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "read-only-cold": { + "calls": [], + "enqueued": [ + { + "locale": "en", + "metric": "bmi", + "userId": "parity-rich" + } + ], + "notes": [], + "result": { + "cached": false, + "hasProvider": true, + "preparing": true, + "revalidating": false, + "text": null, + "updatedAt": null + } + }, + "read-only-stale": { + "calls": [], + "enqueued": [ + { + "locale": "en", + "metric": "bmi", + "userId": "parity-rich" + } + ], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "preparing": false, + "revalidating": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "unparseable-fr": { + "calls": [ + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open with what this means for the user today, then the number behind it.", @@ -691,397 +938,150 @@ "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", "", "", - "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"dein BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" - ] + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"dein BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — BMI:", + "- The snapshot carries bmi.signal (the finished comparison) + bmi.summary + bmi.series (graded). bmi.latestDayFocus shows the latest value, its WHO classification and the step from the prior day; bmi.target is the green band (18.5-24.9).", + "- This card's focus is WHO-BAND placement, not weight pace (the weight card carries that). Centre it on which band the value sits in and whether it crosses a band or nears a band boundary.", + "- WHO bands for rough placement, never as a label for the person: Underweight < 18.5, Normal 18.5-24.9, Overweight 25.0-29.9, Obesity I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. From age 65+, 22-27 counts as favourable (DEGAM).", + "- Band reference: lead from bmi.signal — current, delta vs baselineLabel and placement (band position) are already computed; STATE them, do NOT recompute. Call it out explicitly when the recent value has crossed a WHO band or is nearing a band boundary (within ~0.5 BMI points). When the value stays safely inside the same band, say so briefly — do not force a pace narrative.", + "- Name BMI's limits plainly: it does not measure body composition and is weakly informative for muscular or very athletic people.", + "- One message: close with ONE doable step that fits the band placement ONLY when the finding implies one — when the value is stable in the favourable band, acknowledge that honestly and name one thing worth keeping an eye on rather than forcing a finding or a step.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "temperature": 0.45, "timeoutMs": 60000 } ], "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "de", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "items": null, + "locale": "en", + "metric": "bmi", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "bmi", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], "result": { + "cached": true, "hasProvider": false, - "text": "Últimamente algo fuera de tu rango habitual. Tu IMC está en 26,6 ahora mismo — por encima de tu promedio habitual de 26,1. Léelo junto con la tendencia de tu peso y tu grasa corporal a lo largo de unas semanas, no día a día.", - "cached": true, + "text": "Un peu en dehors de tes repères habituels ces derniers temps. Ton IMC est à 26,6 en ce moment — au-dessus de ta moyenne habituelle de 26,1. Lis-le avec la tendance de ton poids et ta masse grasse sur quelques semaines plutôt qu’au jour le jour.", "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "bare-error-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "BMI assessment currently requires height data in the profile.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "bare-error-de": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Für die BMI-Einschätzung fehlen aktuell Größenangaben im Profil.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "keyless-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "A little off your usual lately. Your BMI is at 30.9 right now — higher than your usual average of 30.3. Read it alongside your weight trend and body-fat over a few weeks rather than day by day.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "keyless-de": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Zuletzt etwas außerhalb deines üblichen Rahmens. Dein BMI liegt aktuell bei 30,9 — höher als dein üblicher Schnitt von 30,3. Schau ihn dir zusammen mit Gewichtstrend und Körperfett über ein paar Wochen an.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "keyless-read-only": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. BMI is a rough orientation rather than a verdict, and it means most read alongside your weight trend and body composition. Sustained movement over weeks carries where a single value does not.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", - "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "bmi", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] + } } } diff --git a/tests/integration/__golden__/status-parity/general.json b/tests/integration/__golden__/status-parity/general.json index 306679ba2..e290135c3 100644 --- a/tests/integration/__golden__/status-parity/general.json +++ b/tests/integration/__golden__/status-parity/general.json @@ -1,388 +1,30 @@ { - "read-only-cold": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "general", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "preparing": true, - "revalidating": false - }, - "notes": [] - }, - "fresh-en": { + "bare-error-de": { "calls": [ { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — OVERALL ASSESSMENT:", - "- The snapshot carries measurementSeries (a map per metric type with summary + graded series), medicationAdherence (summary + series), bloodPressureTargets (when present) and moodContext (when ≥ 3 days). dataCoverage.avgDaysBetweenMeasurements shows the measurement density.", - "- LENGTH (overrides the single-metric budget): this section spans up to six metrics — write 3-5 sentences, roughly 50-80 words. Still concise, no filler.", - "- Pull the available parameters into ONE coherent picture — do not list each separately. Name AT MOST two metrics by name (the most notable ones); mention the rest only when they deviate notably from their own mean. Prioritise: the most notable first, stable values only briefly.", - "- Same logic per metric: place the recent mean against the person's weekly/monthly mean and report only significant deviations from their own baseline.", - "- Acknowledge the positive explicitly — a stable or improving value is a real message, not filler.", - "- Mention associations only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Include mood as context, do not over-weight it.", - "- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion (\"one thing\") ONLY when the overall picture implies one. When everything is steady and there is nothing useful to do, say so honestly and name one thing worth keeping an eye on instead of forcing a recommendation." - ], + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with what this means for the user today, then the number behind it.", - "Write one short overall assessment across the available health metrics. Open with the overall read in plain words — how things are looking taken together, not a number (e.g. \"a steady stretch across the board\", \"one thing standing out this week\") — then bring in the one or two metrics that stand out as support, each placed against the person's own weekly/monthly baseline; never lead with a value. Close with the single most important doable step only when the overall picture genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count, density and recency.", + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Gesamteinschätzung über die verfügbaren Gesundheitsdaten. Beginne mit dem Gesamteindruck in klaren Worten — wie es zusammengenommen aussieht, nicht mit einer Zahl (z. B. \"über alles hinweg eine ruhige Phase\", \"diese Woche sticht eine Sache heraus\") — und bring danach die ein bis zwei auffälligsten Metriken als Beleg, jeweils gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit einem Wert. Schließe nur dann mit dem einen wichtigsten machbaren Schritt, wenn das Gesamtbild wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl, Dichte und Aktualität ableiten.", "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", "", "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", "", - "DATA STRENGTH: 887 day-buckets in the window, latest reading today.", + "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", "", "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" - ] + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null,\"avgDaysBetweenMeasurements\":null},\"measurementSeries\":{},\"medicationAdherence\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"bloodPressureTargets\":null,\"moodContext\":null}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "cached-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "forced-unchanged-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "next-day-en": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — OVERALL ASSESSMENT:", - "- The snapshot carries measurementSeries (a map per metric type with summary + graded series), medicationAdherence (summary + series), bloodPressureTargets (when present) and moodContext (when ≥ 3 days). dataCoverage.avgDaysBetweenMeasurements shows the measurement density.", - "- LENGTH (overrides the single-metric budget): this section spans up to six metrics — write 3-5 sentences, roughly 50-80 words. Still concise, no filler.", - "- Pull the available parameters into ONE coherent picture — do not list each separately. Name AT MOST two metrics by name (the most notable ones); mention the rest only when they deviate notably from their own mean. Prioritise: the most notable first, stable values only briefly.", - "- Same logic per metric: place the recent mean against the person's weekly/monthly mean and report only significant deviations from their own baseline.", - "- Acknowledge the positive explicitly — a stable or improving value is a real message, not filler.", - "- Mention associations only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Include mood as context, do not over-weight it.", - "- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion (\"one thing\") ONLY when the overall picture implies one. When everything is steady and there is nothing useful to do, say so honestly and name one thing worth keeping an eye on instead of forcing a recommendation." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-16", - "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", - "Write one short overall assessment across the available health metrics. Open with the overall read in plain words — how things are looking taken together, not a number (e.g. \"a steady stretch across the board\", \"one thing standing out this week\") — then bring in the one or two metrics that stand out as support, each placed against the person's own weekly/monthly baseline; never lead with a value. Close with the single most important doable step only when the overall picture genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count, density and recency.", - "", - "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", - "Parity note for fresh-en.", - "", - "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 887 day-buckets in the window, latest reading 1 day ago.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":1,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.55,\"max\":83.07,\"mean\":82.88,\"n\":9}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":117,\"max\":133,\"mean\":127.25,\"n\":12}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":81.75,\"n\":12}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":62,\"max\":83,\"mean\":71.88,\"n\":49}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.76,\"n\":17}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":87.5,\"n\":8}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"monthly\":[]}}}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 250, "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for next-day-en.", - "cached": false, - "updatedAt": "2026-06-16T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "read-only-stale": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "general", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": "Parity note for next-day-en.", - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z", - "preparing": false, - "revalidating": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "prose-de": { - "calls": [ - { "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -468,72 +110,124 @@ "- Zusammenhänge nur erwähnen, wenn ein r-Wert im Snapshot vorhanden und |r| > 0.4 ist — als Zusammenhang, nie als Ursache. Stimmung als Kontext einbeziehen, nicht übergewichten.", "- Eine Botschaft: Auch über alle Metriken hinweg endet die Einschätzung NUR DANN mit GENAU EINER wichtigsten, machbaren Empfehlung (\"eine Sache\"), wenn das Gesamtbild eine nahelegt. Ist alles stabil und gibt es nichts Sinnvolles zu tun, sage das ehrlich und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt eine Empfehlung zu erzwingen." ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", - "Schreibe eine kurze Gesamteinschätzung über die verfügbaren Gesundheitsdaten. Beginne mit dem Gesamteindruck in klaren Worten — wie es zusammengenommen aussieht, nicht mit einer Zahl (z. B. \"über alles hinweg eine ruhige Phase\", \"diese Woche sticht eine Sache heraus\") — und bring danach die ein bis zwei auffälligsten Metriken als Beleg, jeweils gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit einem Wert. Schließe nur dann mit dem einen wichtigsten machbaren Schritt, wenn das Gesamtbild wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl, Dichte und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 887 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": true, - "text": "Das ist kein JSON.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, "notes": [ { - "userId": "parity-rich", - "metric": "general", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "general", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" }, { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": null, "inputHash": null, - "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "general", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "general", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Für diese Karte liegt gerade keine Einschätzung vor. Aussagekräftig werden diese Zahlen über mehrere Wochen, nicht über einzelne Tage — und zu konstanten Zeitpunkten erfasst, damit sie vergleichbar bleiben. Das Signal, das zählt, ist mehrere Kennzahlen, die sich gleichzeitig in dieselbe ungünstige Richtung bewegen, nicht ein Wert an einem Tag.", + "updatedAt": null + } }, - "unparseable-fr": { + "bare-error-en": { "calls": [ { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short overall assessment across the available health metrics. Open with the overall read in plain words — how things are looking taken together, not a number (e.g. \"a steady stretch across the board\", \"one thing standing out this week\") — then bring in the one or two metrics that stand out as support, each placed against the person's own weekly/monthly baseline; never lead with a value. Close with the single most important doable step only when the overall picture genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count, density and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null,\"avgDaysBetweenMeasurements\":null},\"measurementSeries\":{},\"medicationAdherence\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"bloodPressureTargets\":null,\"moodContext\":null}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -607,7 +301,7 @@ "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", "{ \"summary\": \"...\" }", "", "METRIC — OVERALL ASSESSMENT:", @@ -617,13 +311,93 @@ "- Same logic per metric: place the recent mean against the person's weekly/monthly mean and report only significant deviations from their own baseline.", "- Acknowledge the positive explicitly — a stable or improving value is a real message, not filler.", "- Mention associations only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Include mood as context, do not over-weight it.", - "- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion (\"one thing\") ONLY when the overall picture implies one. When everything is steady and there is nothing useful to do, say so honestly and name one thing worth keeping an eye on instead of forcing a recommendation.", - "", - "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + "- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion (\"one thing\") ONLY when the overall picture implies one. When everything is steady and there is nothing useful to do, say so honestly and name one thing worth keeping an eye on instead of forcing a recommendation." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "general", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "general", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", + "updatedAt": null + } + }, + "blocked-es": { + "calls": [ + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open with what this means for the user today, then the number behind it.", @@ -637,69 +411,13 @@ "DATENLAGE: 887 Tages-Buckets im Fenster, letzter Wert heute.", "", "", - "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" - ] + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Aucune évaluation disponible pour le moment. Ces chiffres deviennent lisibles sur plusieurs semaines plutôt que sur des journées isolées, et relevés à des moments constants pour rester comparables. Le signal qui compte, c’est plusieurs indicateurs qui dérivent en même temps dans le même sens défavorable, pas une valeur un jour donné.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "general", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "blocked-es": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -787,9 +505,128 @@ "", "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "general", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "general", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Ahora mismo no hay ninguna valoración disponible. Estos números se vuelven legibles a lo largo de varias semanas y no en días sueltos, y tomados a horas constantes para que sigan siendo comparables. La señal que cuenta son varios indicadores derivando a la vez en la misma dirección desfavorable, no un valor en un día.", + "updatedAt": null + } + }, + "cached-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "fresh-en": { + "calls": [ + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open with what this means for the user today, then the number behind it.", @@ -798,87 +635,18 @@ "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", "", "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", "", - "DATENLAGE: 887 Tages-Buckets im Fenster, letzter Wert heute.", + "DATA STRENGTH: 887 day-buckets in the window, latest reading today.", "", "", - "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" - ] + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Ahora mismo no hay ninguna valoración disponible. Estos números se vuelven legibles a lo largo de varias semanas y no en días sueltos, y tomados a horas constantes para que sigan siendo comparables. La señal que cuenta son varios indicadores derivando a la vez en la misma dirección desfavorable, no un valor en un día.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "general", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "bare-error-en": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -964,111 +732,470 @@ "- Mention associations only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Include mood as context, do not over-weight it.", "- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion (\"one thing\") ONLY when the overall picture implies one. When everything is steady and there is nothing useful to do, say so honestly and name one thing worth keeping an eye on instead of forcing a recommendation." ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with what this means for the user today, then the number behind it.", - "Write one short overall assessment across the available health metrics. Open with the overall read in plain words — how things are looking taken together, not a number (e.g. \"a steady stretch across the board\", \"one thing standing out this week\") — then bring in the one or two metrics that stand out as support, each placed against the person's own weekly/monthly baseline; never lead with a value. Close with the single most important doable step only when the overall picture genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count, density and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null,\"avgDaysBetweenMeasurements\":null},\"measurementSeries\":{},\"medicationAdherence\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"bloodPressureTargets\":null,\"moodContext\":null}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "general", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "general", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" }, { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": null, "inputHash": null, - "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "general", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "general", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "general", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "general", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Für diese Karte liegt gerade keine Einschätzung vor. Aussagekräftig werden diese Zahlen über mehrere Wochen, nicht über einzelne Tage — und zu konstanten Zeitpunkten erfasst, damit sie vergleichbar bleiben. Das Signal, das zählt, ist mehrere Kennzahlen, die sich gleichzeitig in dieselbe ungünstige Richtung bewegen, nicht ein Wert an einem Tag.", + "updatedAt": null + } }, - "bare-error-de": { + "keyless-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "general", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "general", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "general", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", + "updatedAt": null + } + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "general", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "general", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "general", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", + "updatedAt": null + } + }, + "next-day-en": { "calls": [ { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short overall assessment across the available health metrics. Open with the overall read in plain words — how things are looking taken together, not a number (e.g. \"a steady stretch across the board\", \"one thing standing out this week\") — then bring in the one or two metrics that stand out as support, each placed against the person's own weekly/monthly baseline; never lead with a value. Close with the single most important doable step only when the overall picture genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count, density and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 887 day-buckets in the window, latest reading 1 day ago.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":1,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.55,\"max\":83.07,\"mean\":82.88,\"n\":9}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":117,\"max\":133,\"mean\":127.25,\"n\":12}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":81.75,\"n\":12}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":62,\"max\":83,\"mean\":71.88,\"n\":49}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.76,\"n\":17}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":87.5,\"n\":8}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"monthly\":[]}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — OVERALL ASSESSMENT:", + "- The snapshot carries measurementSeries (a map per metric type with summary + graded series), medicationAdherence (summary + series), bloodPressureTargets (when present) and moodContext (when ≥ 3 days). dataCoverage.avgDaysBetweenMeasurements shows the measurement density.", + "- LENGTH (overrides the single-metric budget): this section spans up to six metrics — write 3-5 sentences, roughly 50-80 words. Still concise, no filler.", + "- Pull the available parameters into ONE coherent picture — do not list each separately. Name AT MOST two metrics by name (the most notable ones); mention the rest only when they deviate notably from their own mean. Prioritise: the most notable first, stable values only briefly.", + "- Same logic per metric: place the recent mean against the person's weekly/monthly mean and report only significant deviations from their own baseline.", + "- Acknowledge the positive explicitly — a stable or improving value is a real message, not filler.", + "- Mention associations only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Include mood as context, do not over-weight it.", + "- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion (\"one thing\") ONLY when the overall picture implies one. When everything is steady and there is nothing useful to do, say so honestly and name one thing worth keeping an eye on instead of forcing a recommendation." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "prose-de": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Gesamteinschätzung über die verfügbaren Gesundheitsdaten. Beginne mit dem Gesamteindruck in klaren Worten — wie es zusammengenommen aussieht, nicht mit einer Zahl (z. B. \"über alles hinweg eine ruhige Phase\", \"diese Woche sticht eine Sache heraus\") — und bring danach die ein bis zwei auffälligsten Metriken als Beleg, jeweils gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit einem Wert. Schließe nur dann mit dem einen wichtigsten machbaren Schritt, wenn das Gesamtbild wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl, Dichte und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 887 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -1154,389 +1281,262 @@ "- Zusammenhänge nur erwähnen, wenn ein r-Wert im Snapshot vorhanden und |r| > 0.4 ist — als Zusammenhang, nie als Ursache. Stimmung als Kontext einbeziehen, nicht übergewichten.", "- Eine Botschaft: Auch über alle Metriken hinweg endet die Einschätzung NUR DANN mit GENAU EINER wichtigsten, machbaren Empfehlung (\"eine Sache\"), wenn das Gesamtbild eine nahelegt. Ist alles stabil und gibt es nichts Sinnvolles zu tun, sage das ehrlich und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt eine Empfehlung zu erzwingen." ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", - "Schreibe eine kurze Gesamteinschätzung über die verfügbaren Gesundheitsdaten. Beginne mit dem Gesamteindruck in klaren Worten — wie es zusammengenommen aussieht, nicht mit einer Zahl (z. B. \"über alles hinweg eine ruhige Phase\", \"diese Woche sticht eine Sache heraus\") — und bring danach die ein bis zwei auffälligsten Metriken als Beleg, jeweils gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit einem Wert. Schließe nur dann mit dem einen wichtigsten machbaren Schritt, wenn das Gesamtbild wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl, Dichte und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", - "", - "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null,\"avgDaysBetweenMeasurements\":null},\"measurementSeries\":{},\"medicationAdherence\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"bloodPressureTargets\":null,\"moodContext\":null}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "Für diese Karte liegt gerade keine Einschätzung vor. Aussagekräftig werden diese Zahlen über mehrere Wochen, nicht über einzelne Tage — und zu konstanten Zeitpunkten erfasst, damit sie vergleichbar bleiben. Das Signal, das zählt, ist mehrere Kennzahlen, die sich gleichzeitig in dieselbe ungünstige Richtung bewegen, nicht ein Wert an einem Tag.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "general", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "general", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "general", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" } - ] + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Das ist kein JSON.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } }, - "keyless-en": { + "read-only-cold": { "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", - "cached": true, - "updatedAt": null - }, - "notes": [ + "enqueued": [ { - "userId": "parity-bare", - "metric": "general", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "general", "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", "metric": "general", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "userId": "parity-rich" } - ] + ], + "notes": [], + "result": { + "cached": false, + "hasProvider": true, + "preparing": true, + "revalidating": false, + "text": null, + "updatedAt": null + } }, - "keyless-de": { + "read-only-stale": { "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Für diese Karte liegt gerade keine Einschätzung vor. Aussagekräftig werden diese Zahlen über mehrere Wochen, nicht über einzelne Tage — und zu konstanten Zeitpunkten erfasst, damit sie vergleichbar bleiben. Das Signal, das zählt, ist mehrere Kennzahlen, die sich gleichzeitig in dieselbe ungünstige Richtung bewegen, nicht ein Wert an einem Tag.", - "cached": true, - "updatedAt": null - }, + "enqueued": [ + { + "locale": "en", + "metric": "general", + "userId": "parity-rich" + } + ], "notes": [ { - "userId": "parity-bare", - "metric": "general", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "general", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": true, + "preparing": false, + "revalidating": true, + "text": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } }, - "keyless-read-only": { - "calls": [], + "unparseable-fr": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short overall assessment across the available health metrics. Open with the overall read in plain words — how things are looking taken together, not a number (e.g. \"a steady stretch across the board\", \"one thing standing out this week\") — then bring in the one or two metrics that stand out as support, each placed against the person's own weekly/monthly baseline; never lead with a value. Close with the single most important doable step only when the overall picture genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count, density and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 887 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — OVERALL ASSESSMENT:", + "- The snapshot carries measurementSeries (a map per metric type with summary + graded series), medicationAdherence (summary + series), bloodPressureTargets (when present) and moodContext (when ≥ 3 days). dataCoverage.avgDaysBetweenMeasurements shows the measurement density.", + "- LENGTH (overrides the single-metric budget): this section spans up to six metrics — write 3-5 sentences, roughly 50-80 words. Still concise, no filler.", + "- Pull the available parameters into ONE coherent picture — do not list each separately. Name AT MOST two metrics by name (the most notable ones); mention the rest only when they deviate notably from their own mean. Prioritise: the most notable first, stable values only briefly.", + "- Same logic per metric: place the recent mean against the person's weekly/monthly mean and report only significant deviations from their own baseline.", + "- Acknowledge the positive explicitly — a stable or improving value is a real message, not filler.", + "- Mention associations only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Include mood as context, do not over-weight it.", + "- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion (\"one thing\") ONLY when the overall picture implies one. When everything is steady and there is nothing useful to do, say so honestly and name one thing worth keeping an eye on instead of forcing a recommendation.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "general", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "general", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "general", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "general", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "general", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "general", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "general", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "general", + "inputHash": null, + "items": null, "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "general", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Aucune évaluation disponible pour le moment. Ces chiffres deviennent lisibles sur plusieurs semaines plutôt que sur des journées isolées, et relevés à des moments constants pour rester comparables. Le signal qui compte, c’est plusieurs indicateurs qui dérivent en même temps dans le même sens défavorable, pas une valeur un jour donné.", + "updatedAt": null + } } } diff --git a/tests/integration/__golden__/status-parity/medication-compliance.json b/tests/integration/__golden__/status-parity/medication-compliance.json index 9c1e27a6b..ee8a8edd3 100644 --- a/tests/integration/__golden__/status-parity/medication-compliance.json +++ b/tests/integration/__golden__/status-parity/medication-compliance.json @@ -1,610 +1,12 @@ { - "read-only-cold": { + "bare-error-de": { "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "summary": null, - "medications": [], - "cached": false, - "updatedAt": null, - "preparing": true, - "revalidating": false - }, - "notes": [] - }, - "fresh-en": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — MEDICATION ADHERENCE:", - "- The snapshot carries overall (medicationCount, averageCompliance7, averageCompliance30) and medications[] per medication with name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (the finished trend comparison), dailySeries (graded) and latestDay.", - "- Read adherence as the percentage of scheduled doses taken over the last 7 and 30 days; compliance7/compliance30 are server-computed. For the recent DIRECTION against the person's own baseline, lead from medications[].signal — current, delta vs baselineLabel and outsideNormalSwing are already computed; STATE them, do NOT recompute.", - "- Rare cadence: with a low schedulesPerDay or an infrequent schedule (e.g. a weekly injection), compliance7 is noise-prone — a single missed dose swings the percentage sharply. Lean the assessment on compliance30 (the load-bearing value) and treat compliance7 only as a rough pointer.", - "- Placement (for orientation, not judgement): ≥ 90% very reliable, 80-89% good, 70-79% patchy, < 70% clearly patchy — therapeutic effect may then be reduced.", - "- Frame positively: streak is the longest uninterrupted run — acknowledge a good streak explicitly. With several medications, name the one with the lowest adherence.", - "- Mention links to vital signs only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Do not invent an effect when the data is absent.", - "- One message: close with ONE doable step that fits the gap ONLY when the finding implies one (e.g. evening doses are missed more often — a fixed routine or a reminder at the critical time can help). When adherence is consistently high, acknowledge that honestly rather than manufacturing a step. No blame." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with the direction of change over time, then say where it stands now.", - "Write one short assessment of this person's medication adherence. Open with what it MEANS in plain words — how reliable the routine is looking, not the number (e.g. \"your routine's been rock-solid\", \"a few doses have slipped lately\") — then bring in the recent rate right after as support, placed against their own baseline (compliance7 vs compliance30); never lead with the value. Close with one doable, blame-free step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the event count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 176 day-buckets in the window, latest reading 1 day ago.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"adherence for Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"adherence for Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], "enqueued": [], - "result": { - "hasProvider": true, - "summary": "Parity note for fresh-en.", - "medications": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, "notes": [ { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "inputHash": null, - "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "cached-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "summary": "Parity note for fresh-en.", - "medications": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], "inputHash": null, - "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "forced-unchanged-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "summary": "Parity note for fresh-en.", - "medications": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "inputHash": null, - "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "next-day-en": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — MEDICATION ADHERENCE:", - "- The snapshot carries overall (medicationCount, averageCompliance7, averageCompliance30) and medications[] per medication with name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (the finished trend comparison), dailySeries (graded) and latestDay.", - "- Read adherence as the percentage of scheduled doses taken over the last 7 and 30 days; compliance7/compliance30 are server-computed. For the recent DIRECTION against the person's own baseline, lead from medications[].signal — current, delta vs baselineLabel and outsideNormalSwing are already computed; STATE them, do NOT recompute.", - "- Rare cadence: with a low schedulesPerDay or an infrequent schedule (e.g. a weekly injection), compliance7 is noise-prone — a single missed dose swings the percentage sharply. Lean the assessment on compliance30 (the load-bearing value) and treat compliance7 only as a rough pointer.", - "- Placement (for orientation, not judgement): ≥ 90% very reliable, 80-89% good, 70-79% patchy, < 70% clearly patchy — therapeutic effect may then be reduced.", - "- Frame positively: streak is the longest uninterrupted run — acknowledge a good streak explicitly. With several medications, name the one with the lowest adherence.", - "- Mention links to vital signs only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Do not invent an effect when the data is absent.", - "- One message: close with ONE doable step that fits the gap ONLY when the finding implies one (e.g. evening doses are missed more often — a fixed routine or a reminder at the critical time can help). When adherence is consistently high, acknowledge that honestly rather than manufacturing a step. No blame." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-16", - "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", - "Write one short assessment of this person's medication adherence. Open with what it MEANS in plain words — how reliable the routine is looking, not the number (e.g. \"your routine's been rock-solid\", \"a few doses have slipped lately\") — then bring in the recent rate right after as support, placed against their own baseline (compliance7 vs compliance30); never lead with the value. Close with one doable, blame-free step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the event count and recency.", - "", - "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", - "Parity note for fresh-en.", - "", - "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 176 day-buckets in the window, latest reading 2 days ago.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":2},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":76,\"averageCompliance7\":50},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":50,\"compliance30\":76,\"streak\":0,\"taken7\":3,\"skipped7\":0,\"missed7\":3,\"signal\":{\"metric\":\"adherence for Ramipril\",\"unit\":\"%\",\"current\":75,\"currentWindowDays\":20,\"baseline\":23.53,\"baselineLabel\":\"your monthly average\",\"delta\":51.47,\"deltaPct\":218.7,\"spread\":33.28,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-15\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":80,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":2}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":47.06,\"n\":17}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":0,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":50,\"compliance30\":76,\"streak\":0,\"taken7\":3,\"skipped7\":0,\"missed7\":3,\"signal\":{\"metric\":\"adherence for Vitamin D\",\"unit\":\"%\",\"current\":75,\"currentWindowDays\":20,\"baseline\":23.53,\"baselineLabel\":\"your monthly average\",\"delta\":51.47,\"deltaPct\":218.7,\"spread\":33.28,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-15\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":80,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":2}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":47.06,\"n\":17}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":0,\"n\":1}}]}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "summary": "Parity note for next-day-en.", - "medications": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "cached": false, - "updatedAt": "2026-06-16T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "inputHash": null, - "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "read-only-stale": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "summary": "Parity note for next-day-en.", - "medications": [], - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z", - "preparing": false, - "revalidating": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "inputHash": null, - "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "prose-de": { - "calls": [ - { - "system": [ - "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", - "", - "DEINE DATENGRUNDLAGE (graded snapshot):", - "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", - "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", - "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", - "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", - "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", - "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", - "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", - "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", - "", - "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", - "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", - "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", - "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", - "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", - "(d) einen Punkt zum Im-Auge-behalten;", - "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", - "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", - "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", - "", - "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", - "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", - "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", - "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", - "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", - "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", - "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", - "", - "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", - "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", - "", - "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", - "", - "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", - "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", - "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", - "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", - "", - "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", - "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", - "", - "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", - "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", - "", - "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", - "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", - "", - "AKUTE WARNZEICHEN (Vertrag, kein Stil)", - "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", - "", - "ERDUNG — NULL HALLUZINATIONEN", - "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", - "", - "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", - "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", - "", - "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", - "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", - "", - "EINORDNEN, NICHT AUFZÄHLEN", - "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", - "", - "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", - "", - "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", - "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", - "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", - "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", - "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", - "", - "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", - "{ \"summary\": \"...\" }", - "", - "METRIK — MEDIKAMENTEN-EINNAHMETREUE:", - "- Der Snapshot trägt overall (medicationCount, averageCompliance7, averageCompliance30) und medications[] je Medikament mit name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (der fertige Trend-Vergleich), dailySeries (graded) und latestDay.", - "- Lies die Einnahmetreue als Prozentsatz erfüllter geplanter Dosen über die letzten 7 bzw. 30 Tage; compliance7/compliance30 sind server-berechnet. Für die jüngste RICHTUNG gegen die eigene Baseline führe mit medications[].signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet; NENNE sie, rechne sie NICHT neu.", - "- Seltene Kadenz: Bei niedriger schedulesPerDay oder seltener Einnahme (z.B. Wocheninjektion) ist compliance7 rauschanfällig — eine einzige verpasste Dosis verschiebt den Prozentwert stark. Stütze die Einschätzung dann auf compliance30 (den tragenden Wert) und behandle compliance7 nur als groben Hinweis.", - "- Einordnung (zur Orientierung, nicht als Urteil): ≥ 90 % sehr verlässlich, 80-89 % gut, 70-79 % lückenhaft, < 70 % deutlich lückenhaft — die therapeutische Wirkung kann dann eingeschränkt sein.", - "- Positiv rahmen: streak ist die längste ununterbrochene Serie — bei guter Treue ausdrücklich anerkennen. Bei mehreren Medikamenten das mit der niedrigsten Treue benennen.", - "- Zusammenhänge zu Vitalwerten nur erwähnen, wenn ein r-Wert im Snapshot vorhanden und |r| > 0.4 ist — als Zusammenhang, nie als Ursache. Keine Wirkung erfinden, wenn die Daten fehlen.", - "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. Abenddosen werden häufiger vergessen — eine feste Routine oder ein Reminder zur kritischen Uhrzeit kann helfen). Bei durchgehend hoher Treue das ehrlich anerkennen, statt einen Schritt zu erzwingen. Keine Schuldzuweisung." - ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne mit der Richtung der Veränderung über die Zeit und nenne dann den aktuellen Stand.", - "Schreibe eine kurze Einschätzung zur Einnahmetreue dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — wie verlässlich die Routine aussieht, nicht der Zahl (z. B. \"deine Routine sitzt richtig gut\", \"zuletzt sind ein paar Dosen durchgerutscht\") — und bring danach die jüngste Rate als Beleg, gegen die eigene Baseline eingeordnet (compliance7 vs. compliance30); führe nie mit dem Wert. Schließe nur dann mit einem machbaren, wertfreien Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Ereignisanzahl und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 176 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", - "", - "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "summary": "Das ist kein JSON.", - "medications": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - } - ], - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "de", - "text": "Das ist kein JSON.", "items": [ { "medicationId": "parity-rich-med-bp", @@ -615,199 +17,18 @@ "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." } ], - "inputHash": null, - "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "inputHash": null, - "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "unparseable-fr": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — MEDICATION ADHERENCE:", - "- The snapshot carries overall (medicationCount, averageCompliance7, averageCompliance30) and medications[] per medication with name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (the finished trend comparison), dailySeries (graded) and latestDay.", - "- Read adherence as the percentage of scheduled doses taken over the last 7 and 30 days; compliance7/compliance30 are server-computed. For the recent DIRECTION against the person's own baseline, lead from medications[].signal — current, delta vs baselineLabel and outsideNormalSwing are already computed; STATE them, do NOT recompute.", - "- Rare cadence: with a low schedulesPerDay or an infrequent schedule (e.g. a weekly injection), compliance7 is noise-prone — a single missed dose swings the percentage sharply. Lean the assessment on compliance30 (the load-bearing value) and treat compliance7 only as a rough pointer.", - "- Placement (for orientation, not judgement): ≥ 90% very reliable, 80-89% good, 70-79% patchy, < 70% clearly patchy — therapeutic effect may then be reduced.", - "- Frame positively: streak is the longest uninterrupted run — acknowledge a good streak explicitly. With several medications, name the one with the lowest adherence.", - "- Mention links to vital signs only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Do not invent an effect when the data is absent.", - "- One message: close with ONE doable step that fits the gap ONLY when the finding implies one (e.g. evening doses are missed more often — a fixed routine or a reminder at the critical time can help). When adherence is consistently high, acknowledge that honestly rather than manufacturing a step. No blame.", - "", - "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with the direction of change over time, then say where it stands now.", - "Write one short assessment of this person's medication adherence. Open with what it MEANS in plain words — how reliable the routine is looking, not the number (e.g. \"your routine's been rock-solid\", \"a few doses have slipped lately\") — then bring in the recent rate right after as support, placed against their own baseline (compliance7 vs compliance30); never lead with the value. Close with one doable, blame-free step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the event count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 176 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", - "", - "", - "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "summary": "Aucune évaluation disponible pour le moment. L’observance se lit comme une régularité sur des semaines plutôt que comme une série de journées parfaites — par médicament et dans l’ensemble. Les oublis répétés à la même heure sont le motif qui compte, et c’est une routine fixe qui les rattrape d’ordinaire.", - "medications": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", "locale": "de", - "text": "Das ist kein JSON.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - } - ], - "inputHash": null, - "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", + "metric": "medication-compliance", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Parity note for next-day-en.", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, "items": [ { "medicationId": "parity-rich-med-bp", @@ -818,31 +39,158 @@ "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." } ], - "inputHash": null, - "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "medication-compliance", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "medication-compliance", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": true, + "medications": [], + "summary": "Aktuell sind keine aktiven Medikamente hinterlegt.", + "updatedAt": null + } + }, + "bare-error-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "locale": "de", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "medication-compliance", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "medication-compliance", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "medications": [], + "summary": "There are currently no active medications configured.", + "updatedAt": null + } }, "blocked-es": { "calls": [ { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's medication adherence. Open with what it MEANS in plain words — how reliable the routine is looking, not the number (e.g. \"your routine's been rock-solid\", \"a few doses have slipped lately\") — then bring in the recent rate right after as support, placed against their own baseline (compliance7 vs compliance30); never lead with the value. Close with one doable, blame-free step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the event count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 176 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", + "", + "", + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -930,9 +278,1002 @@ "", "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "locale": "de", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "medication-compliance", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "medication-compliance", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "medications": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "summary": "Ahora mismo no hay ninguna valoración disponible. La adherencia se lee como constancia a lo largo de semanas y no como una racha de días perfectos — por medicamento y en el conjunto. Los olvidos repetidos a la misma hora son el patrón que cuenta, y lo que suele recuperarlos es una rutina fija.", + "updatedAt": null + } + }, + "cached-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "medications": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "summary": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "medications": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "summary": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "fresh-en": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's medication adherence. Open with what it MEANS in plain words — how reliable the routine is looking, not the number (e.g. \"your routine's been rock-solid\", \"a few doses have slipped lately\") — then bring in the recent rate right after as support, placed against their own baseline (compliance7 vs compliance30); never lead with the value. Close with one doable, blame-free step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the event count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 176 day-buckets in the window, latest reading 1 day ago.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"adherence for Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"adherence for Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MEDICATION ADHERENCE:", + "- The snapshot carries overall (medicationCount, averageCompliance7, averageCompliance30) and medications[] per medication with name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (the finished trend comparison), dailySeries (graded) and latestDay.", + "- Read adherence as the percentage of scheduled doses taken over the last 7 and 30 days; compliance7/compliance30 are server-computed. For the recent DIRECTION against the person's own baseline, lead from medications[].signal — current, delta vs baselineLabel and outsideNormalSwing are already computed; STATE them, do NOT recompute.", + "- Rare cadence: with a low schedulesPerDay or an infrequent schedule (e.g. a weekly injection), compliance7 is noise-prone — a single missed dose swings the percentage sharply. Lean the assessment on compliance30 (the load-bearing value) and treat compliance7 only as a rough pointer.", + "- Placement (for orientation, not judgement): ≥ 90% very reliable, 80-89% good, 70-79% patchy, < 70% clearly patchy — therapeutic effect may then be reduced.", + "- Frame positively: streak is the longest uninterrupted run — acknowledge a good streak explicitly. With several medications, name the one with the lowest adherence.", + "- Mention links to vital signs only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Do not invent an effect when the data is absent.", + "- One message: close with ONE doable step that fits the gap ONLY when the finding implies one (e.g. evening doses are missed more often — a fixed routine or a reminder at the critical time can help). When adherence is consistently high, acknowledge that honestly rather than manufacturing a step. No blame." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "medications": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "summary": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "locale": "de", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "medication-compliance", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "medication-compliance", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "medications": [], + "summary": "Für diese Karte liegt gerade keine Einschätzung vor. Einnahmetreue liest sich als Konstanz über Wochen, nicht als eine Reihe perfekter Tage — je Medikament und im Gesamtbild. Wiederkehrende Auslassungen zur selben Tageszeit sind das Muster, auf das es ankommt, und eine feste Routine fängt sie am ehesten ab.", + "updatedAt": null + } + }, + "keyless-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "locale": "de", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "medication-compliance", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "medication-compliance", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "medications": [], + "summary": "No assessment on this one right now. Adherence reads as consistency over weeks rather than a run of perfect days — per medication and in the overall picture. Repeated misses at the same time of day are the pattern that counts, and a fixed routine is what usually catches them.", + "updatedAt": null + } + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "locale": "de", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "medication-compliance", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "medication-compliance", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "medications": [], + "summary": "No assessment on this one right now. Adherence reads as consistency over weeks rather than a run of perfect days — per medication and in the overall picture. Repeated misses at the same time of day are the pattern that counts, and a fixed routine is what usually catches them.", + "updatedAt": null + } + }, + "next-day-en": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's medication adherence. Open with what it MEANS in plain words — how reliable the routine is looking, not the number (e.g. \"your routine's been rock-solid\", \"a few doses have slipped lately\") — then bring in the recent rate right after as support, placed against their own baseline (compliance7 vs compliance30); never lead with the value. Close with one doable, blame-free step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the event count and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 176 day-buckets in the window, latest reading 2 days ago.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":2},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":76,\"averageCompliance7\":50},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":50,\"compliance30\":76,\"streak\":0,\"taken7\":3,\"skipped7\":0,\"missed7\":3,\"signal\":{\"metric\":\"adherence for Ramipril\",\"unit\":\"%\",\"current\":75,\"currentWindowDays\":20,\"baseline\":23.53,\"baselineLabel\":\"your monthly average\",\"delta\":51.47,\"deltaPct\":218.7,\"spread\":33.28,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-15\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":80,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":2}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":47.06,\"n\":17}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":0,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":50,\"compliance30\":76,\"streak\":0,\"taken7\":3,\"skipped7\":0,\"missed7\":3,\"signal\":{\"metric\":\"adherence for Vitamin D\",\"unit\":\"%\",\"current\":75,\"currentWindowDays\":20,\"baseline\":23.53,\"baselineLabel\":\"your monthly average\",\"delta\":51.47,\"deltaPct\":218.7,\"spread\":33.28,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-15\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":80,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":2}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":47.06,\"n\":17}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":0,\"n\":1}}]}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MEDICATION ADHERENCE:", + "- The snapshot carries overall (medicationCount, averageCompliance7, averageCompliance30) and medications[] per medication with name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (the finished trend comparison), dailySeries (graded) and latestDay.", + "- Read adherence as the percentage of scheduled doses taken over the last 7 and 30 days; compliance7/compliance30 are server-computed. For the recent DIRECTION against the person's own baseline, lead from medications[].signal — current, delta vs baselineLabel and outsideNormalSwing are already computed; STATE them, do NOT recompute.", + "- Rare cadence: with a low schedulesPerDay or an infrequent schedule (e.g. a weekly injection), compliance7 is noise-prone — a single missed dose swings the percentage sharply. Lean the assessment on compliance30 (the load-bearing value) and treat compliance7 only as a rough pointer.", + "- Placement (for orientation, not judgement): ≥ 90% very reliable, 80-89% good, 70-79% patchy, < 70% clearly patchy — therapeutic effect may then be reduced.", + "- Frame positively: streak is the longest uninterrupted run — acknowledge a good streak explicitly. With several medications, name the one with the lowest adherence.", + "- Mention links to vital signs only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Do not invent an effect when the data is absent.", + "- One message: close with ONE doable step that fits the gap ONLY when the finding implies one (e.g. evening doses are missed more often — a fixed routine or a reminder at the critical time can help). When adherence is consistently high, acknowledge that honestly rather than manufacturing a step. No blame." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "medications": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "summary": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "prose-de": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Richtung der Veränderung über die Zeit und nenne dann den aktuellen Stand.", + "Schreibe eine kurze Einschätzung zur Einnahmetreue dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — wie verlässlich die Routine aussieht, nicht der Zahl (z. B. \"deine Routine sitzt richtig gut\", \"zuletzt sind ein paar Dosen durchgerutscht\") — und bring danach die jüngste Rate als Beleg, gegen die eigene Baseline eingeordnet (compliance7 vs. compliance30); führe nie mit dem Wert. Schließe nur dann mit einem machbaren, wertfreien Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Ereignisanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 176 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — MEDIKAMENTEN-EINNAHMETREUE:", + "- Der Snapshot trägt overall (medicationCount, averageCompliance7, averageCompliance30) und medications[] je Medikament mit name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (der fertige Trend-Vergleich), dailySeries (graded) und latestDay.", + "- Lies die Einnahmetreue als Prozentsatz erfüllter geplanter Dosen über die letzten 7 bzw. 30 Tage; compliance7/compliance30 sind server-berechnet. Für die jüngste RICHTUNG gegen die eigene Baseline führe mit medications[].signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet; NENNE sie, rechne sie NICHT neu.", + "- Seltene Kadenz: Bei niedriger schedulesPerDay oder seltener Einnahme (z.B. Wocheninjektion) ist compliance7 rauschanfällig — eine einzige verpasste Dosis verschiebt den Prozentwert stark. Stütze die Einschätzung dann auf compliance30 (den tragenden Wert) und behandle compliance7 nur als groben Hinweis.", + "- Einordnung (zur Orientierung, nicht als Urteil): ≥ 90 % sehr verlässlich, 80-89 % gut, 70-79 % lückenhaft, < 70 % deutlich lückenhaft — die therapeutische Wirkung kann dann eingeschränkt sein.", + "- Positiv rahmen: streak ist die längste ununterbrochene Serie — bei guter Treue ausdrücklich anerkennen. Bei mehreren Medikamenten das mit der niedrigsten Treue benennen.", + "- Zusammenhänge zu Vitalwerten nur erwähnen, wenn ein r-Wert im Snapshot vorhanden und |r| > 0.4 ist — als Zusammenhang, nie als Ursache. Keine Wirkung erfinden, wenn die Daten fehlen.", + "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. Abenddosen werden häufiger vergessen — eine feste Routine oder ein Reminder zur kritischen Uhrzeit kann helfen). Bei durchgehend hoher Treue das ehrlich anerkennen, statt einen Schritt zu erzwingen. Keine Schuldzuweisung." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "locale": "de", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "medications": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "summary": "Das ist kein JSON.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "read-only-cold": { + "calls": [], + "enqueued": [ + { + "locale": "en", + "metric": "medication-compliance", + "userId": "parity-rich" + } + ], + "notes": [], + "result": { + "cached": false, + "hasProvider": true, + "medications": [], + "preparing": true, + "revalidating": false, + "summary": null, + "updatedAt": null + } + }, + "read-only-stale": { + "calls": [], + "enqueued": [ + { + "locale": "en", + "metric": "medication-compliance", + "userId": "parity-rich" + } + ], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "medications": [], + "preparing": false, + "revalidating": true, + "summary": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "unparseable-fr": { + "calls": [ + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open with the direction of change over time, then say where it stands now.", @@ -946,21 +1287,167 @@ "DATENLAGE: 176 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", "", "", - "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" - ] + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MEDICATION ADHERENCE:", + "- The snapshot carries overall (medicationCount, averageCompliance7, averageCompliance30) and medications[] per medication with name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (the finished trend comparison), dailySeries (graded) and latestDay.", + "- Read adherence as the percentage of scheduled doses taken over the last 7 and 30 days; compliance7/compliance30 are server-computed. For the recent DIRECTION against the person's own baseline, lead from medications[].signal — current, delta vs baselineLabel and outsideNormalSwing are already computed; STATE them, do NOT recompute.", + "- Rare cadence: with a low schedulesPerDay or an infrequent schedule (e.g. a weekly injection), compliance7 is noise-prone — a single missed dose swings the percentage sharply. Lean the assessment on compliance30 (the load-bearing value) and treat compliance7 only as a rough pointer.", + "- Placement (for orientation, not judgement): ≥ 90% very reliable, 80-89% good, 70-79% patchy, < 70% clearly patchy — therapeutic effect may then be reduced.", + "- Frame positively: streak is the longest uninterrupted run — acknowledge a good streak explicitly. With several medications, name the one with the lowest adherence.", + "- Mention links to vital signs only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Do not invent an effect when the data is absent.", + "- One message: close with ONE doable step that fits the gap ONLY when the finding implies one (e.g. evening doses are missed more often — a fixed routine or a reminder at the critical time can help). When adherence is consistently high, acknowledge that honestly rather than manufacturing a step. No blame.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "temperature": 0.45, "timeoutMs": 60000 } ], "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "locale": "de", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "locale": "en", + "metric": "medication-compliance", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "medication-compliance", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], "result": { + "cached": true, "hasProvider": true, - "summary": "Ahora mismo no hay ninguna valoración disponible. La adherencia se lee como constancia a lo largo de semanas y no como una racha de días perfectos — por medicamento y en el conjunto. Los olvidos repetidos a la misma hora son el patrón que cuenta, y lo que suele recuperarlos es una rutina fija.", "medications": [ { "medicationId": "parity-rich-med-bp", @@ -971,495 +1458,8 @@ "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." } ], - "cached": true, + "summary": "Aucune évaluation disponible pour le moment. L’observance se lit comme une régularité sur des semaines plutôt que comme une série de journées parfaites — par médicament et dans l’ensemble. Les oublis répétés à la même heure sont le motif qui compte, et c’est une routine fixe qui les rattrape d’ordinaire.", "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "de", - "text": "Das ist kein JSON.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - } - ], - "inputHash": null, - "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "inputHash": null, - "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "bare-error-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "summary": "There are currently no active medications configured.", - "medications": [], - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "de", - "text": "Das ist kein JSON.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - } - ], - "inputHash": null, - "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "inputHash": null, - "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "bare-error-de": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "summary": "Aktuell sind keine aktiven Medikamente hinterlegt.", - "medications": [], - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "de", - "text": "Das ist kein JSON.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - } - ], - "inputHash": null, - "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "inputHash": null, - "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "keyless-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "summary": "No assessment on this one right now. Adherence reads as consistency over weeks rather than a run of perfect days — per medication and in the overall picture. Repeated misses at the same time of day are the pattern that counts, and a fixed routine is what usually catches them.", - "medications": [], - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "de", - "text": "Das ist kein JSON.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - } - ], - "inputHash": null, - "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "inputHash": null, - "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "keyless-de": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "summary": "Für diese Karte liegt gerade keine Einschätzung vor. Einnahmetreue liest sich als Konstanz über Wochen, nicht als eine Reihe perfekter Tage — je Medikament und im Gesamtbild. Wiederkehrende Auslassungen zur selben Tageszeit sind das Muster, auf das es ankommt, und eine feste Routine fängt sie am ehesten ab.", - "medications": [], - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "de", - "text": "Das ist kein JSON.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - } - ], - "inputHash": null, - "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "inputHash": null, - "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "keyless-read-only": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "summary": "No assessment on this one right now. Adherence reads as consistency over weeks rather than a run of perfect days — per medication and in the overall picture. Repeated misses at the same time of day are the pattern that counts, and a fixed routine is what usually catches them.", - "medications": [], - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "de", - "text": "Das ist kein JSON.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." - } - ], - "inputHash": null, - "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": [ - { - "medicationId": "parity-rich-med-bp", - "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." - }, - { - "medicationId": "parity-rich-med-d", - "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." - } - ], - "inputHash": null, - "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "medication-compliance", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] + } } } diff --git a/tests/integration/__golden__/status-parity/metric.json b/tests/integration/__golden__/status-parity/metric.json index 58b9188c2..fed29fd08 100644 --- a/tests/integration/__golden__/status-parity/metric.json +++ b/tests/integration/__golden__/status-parity/metric.json @@ -1,773 +1,269 @@ { - "RESTING_HEART_RATE read-only-cold": { + "BLOOD_GLUCOSE bare-error-de": { "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "preparing": true, - "revalidating": false - }, - "notes": [] - }, - "RESTING_HEART_RATE fresh-en": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "ARCHETYPE — PHYSIOLOGICAL VITAL:", - "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", - "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", - "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", - "", - "METRIC: Resting heart rate (unit: bpm).", - "Lower values are generally more favourable for this metric.", - "Coarse population reference band: 50–100 bpm (a placement aid only — the user's OWN baseline leads)." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with the direction of change over time, then say where it stands now.", - "Write one short assessment of this person's resting heart rate. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 140 day-buckets in the window, latest reading today.", - "", - "REPETITION: you have already given a similar \"no material change\" assessment for this metric 11 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 12 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", - "", - "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", - "- Higher blood glucose looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=34, r=0.94)", - "- Higher sleep duration looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=89, r=0.86)", - "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", - "", - "", - "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", - "- Current value 60.62 bpm is in the \"the standard resting range\" band, and sits near the LOWER boundary of this band (nearest boundary at 60 bpm).", - "- Guideline bands (AHA 2024; lower values are more favourable):", - " - below 60 bpm: below the standard resting range (common in trained hearts)", - " - 60–100 bpm: the standard resting range", - " - 100 bpm and above: above the standard resting range", - "- Values in the \"the standard resting range\" band are considered a healthy / low-risk range per these guidelines.", - "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", - "", - "", - "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.62,\"currentWindowDays\":21,\"baseline\":60.71,\"baselineLabel\":\"your monthly average\",\"delta\":-0.09,\"deltaPct\":-0.1,\"spread\":1.42,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":21,\"newestDaysAgo\":0,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.67,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":58}}}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, "notes": [ { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "RESTING_HEART_RATE cached-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "RESTING_HEART_RATE forced-unchanged-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for fresh-en.", "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "RESTING_HEART_RATE next-day-en": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "ARCHETYPE — PHYSIOLOGICAL VITAL:", - "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", - "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", - "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", - "", - "METRIC: Resting heart rate (unit: bpm).", - "Lower values are generally more favourable for this metric.", - "Coarse population reference band: 50–100 bpm (a placement aid only — the user's OWN baseline leads)." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-16", - "OPENER HINT: Open with what this means for the user today, then the number behind it.", - "Write one short assessment of this person's resting heart rate. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", - "Parity note for fresh-en.", - "", - "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 140 day-buckets in the window, latest reading 1 day ago.", - "", - "REPETITION: you have already given a similar \"no material change\" assessment for this metric 11 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 12 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", - "", - "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", - "- Higher blood glucose looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=34, r=0.94)", - "- Higher sleep duration looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=89, r=0.86)", - "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", - "", - "", - "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", - "- Current value 60.85 bpm is in the \"the standard resting range\" band, and sits near the LOWER boundary of this band (nearest boundary at 60 bpm).", - "- Guideline bands (AHA 2024; lower values are more favourable):", - " - below 60 bpm: below the standard resting range (common in trained hearts)", - " - 60–100 bpm: the standard resting range", - " - 100 bpm and above: above the standard resting range", - "- Values in the \"the standard resting range\" band are considered a healthy / low-risk range per these guidelines.", - "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", - "", - "", - "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":1},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.85,\"currentWindowDays\":20,\"baseline\":60.65,\"baselineLabel\":\"your monthly average\",\"delta\":0.2,\"deltaPct\":0.3,\"spread\":1.41,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.4,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.76,\"n\":17}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":58}}}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for next-day-en.", - "cached": false, - "updatedAt": "2026-06-16T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "RESTING_HEART_RATE read-only-stale": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": "Parity note for next-day-en.", - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z", - "preparing": false, - "revalidating": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "RESTING_HEART_RATE prose-de": { - "calls": [ - { - "system": [ - "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", - "", - "DEINE DATENGRUNDLAGE (graded snapshot):", - "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", - "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", - "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", - "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", - "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", - "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", - "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", - "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", - "", - "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", - "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", - "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", - "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", - "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", - "(d) einen Punkt zum Im-Auge-behalten;", - "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", - "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", - "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", - "", - "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", - "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", - "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", - "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", - "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", - "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", - "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", - "", - "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", - "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", - "", - "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", - "", - "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", - "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", - "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", - "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", - "", - "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", - "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", - "", - "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", - "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", - "", - "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", - "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", - "", - "AKUTE WARNZEICHEN (Vertrag, kein Stil)", - "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", - "", - "ERDUNG — NULL HALLUZINATIONEN", - "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", - "", - "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", - "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", - "", - "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", - "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", - "", - "EINORDNEN, NICHT AUFZÄHLEN", - "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", - "", - "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", - "", - "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", - "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", - "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", - "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", - "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", - "", - "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", - "{ \"summary\": \"...\" }", - "", - "ARCHETYP — PHYSIOLOGISCHER VITALWERT:", - "- Das ist ein Momentanwert. Benenne das aktuelle Niveau und ordne es sowohl gegen die eigene Baseline (recent vs. Monats-/Jahresmittel) als auch — falls ein Referenzband vorliegt — gegen dieses Band ein.", - "- Ein einzelner Wert außerhalb des Bands ist selten ein Befund; eine anhaltende Verschiebung über das recent-Fenster gegen die Baseline schon. Erwähne einen Trend außerhalb des Bands sachlich, ohne zu alarmieren.", - "- Formuliere jede Richtung (steigend/fallend) als mögliche Erklärung (Fitness, Stress, Schlaf, Krankheit, Medikament), nie als Diagnose.", - "", - "METRIK: Resting heart rate (Einheit: bpm).", - "Niedrigere Werte sind bei dieser Metrik tendenziell günstiger.", - "Grobes Referenzband (Population): 50–100 bpm (nur Orientierung — die EIGENE Baseline führt)." - ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne mit der Richtung der Veränderung über die Zeit und nenne dann den aktuellen Stand.", - "Schreibe eine kurze Einschätzung zu Resting heart rate dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil und genau da, wo es war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 140 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", - "", - "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Wenn der Blutzucker höher liegt, scheint die Ruheherzfrequenz am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=34, r=0.94)", - "- Wenn die Schlafdauer höher liegt, scheint die Ruheherzfrequenz am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=89, r=0.86)", - "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", - "", - "", - "EINORDNUNGS-KONTEXT (Leitlinien-Bänder — serverseitig berechnet; nennen, NICHT neu rechnen):", - "- Der aktuelle Wert 60.62 bpm liegt im Band \"the standard resting range\" und liegt nahe der UNTEREN Grenze dieses Bands (nächste Grenze bei 60 bpm).", - "- Leitlinien-Bänder (AHA 2024; niedrigere Werte sind günstiger):", - " - unter 60 bpm: below the standard resting range (common in trained hearts)", - " - 60–100 bpm: the standard resting range", - " - ab 100 bpm: above the standard resting range", - "- Werte im Band \"the standard resting range\" gelten laut diesen Leitlinien als ein gesunder / risikoarmer Bereich.", - "- Benenne, wo der Wert liegt und was dieses Band praktisch bedeutet. Beurteile jeden Trend GEMÄSS dieser Position: eine Verschiebung tief in einem günstigen Band ist eine Randnotiz; dieselbe Verschiebung nahe an oder über einer Grenze ist die Schlagzeile. Rahme die Konsequenz ohne Diagnose (\"Werte in diesem Bereich gelten als …\", nie \"du hast / bist gefährdet für …\").", - "", - "", - "{\"locale\":\"de\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.62,\"currentWindowDays\":21,\"baseline\":60.71,\"baselineLabel\":\"your monthly average\",\"delta\":-0.09,\"deltaPct\":-0.1,\"spread\":1.42,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":21,\"newestDaysAgo\":0,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.67,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":58}}}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Das ist kein JSON.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "RESTING_HEART_RATE unparseable-fr": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "ARCHETYPE — PHYSIOLOGICAL VITAL:", - "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", - "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", - "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", - "", - "METRIC: Resting heart rate (unit: bpm).", - "Lower values are generally more favourable for this metric.", - "Coarse population reference band: 50–100 bpm (a placement aid only — the user's OWN baseline leads).", - "", - "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with the direction of change over time, then say where it stands now.", - "Write one short assessment of this person's resting heart rate. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 140 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", - "", - "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Quand la glycémie monte, la fréquence cardiaque au repos du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=34, r=0.94)", - "- Quand la durée de sommeil monte, la fréquence cardiaque au repos du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=89, r=0.86)", - "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", - "", - "", - "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", - "- Current value 60.62 bpm is in the \"the standard resting range\" band, and sits near the LOWER boundary of this band (nearest boundary at 60 bpm).", - "- Guideline bands (AHA 2024; lower values are more favourable):", - " - below 60 bpm: below the standard resting range (common in trained hearts)", - " - 60–100 bpm: the standard resting range", - " - 100 bpm and above: above the standard resting range", - "- Values in the \"the standard resting range\" band are considered a healthy / low-risk range per these guidelines.", - "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", - "", - "", - "{\"locale\":\"fr\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.62,\"currentWindowDays\":21,\"baseline\":60.71,\"baselineLabel\":\"your monthly average\",\"delta\":-0.09,\"deltaPct\":-0.1,\"spread\":1.42,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":21,\"newestDaysAgo\":0,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.67,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":58}}}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Stable, comme d’habitude. Resting heart rate est à 60,6 bpm en ce moment — en dessous de ta moyenne habituelle de 60,7 bpm. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", + "items": null, "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": false, + "hasProvider": true, + "insufficient": true, + "text": null, + "updatedAt": null + } }, - "RESTING_HEART_RATE blocked-es": { + "BLOOD_GLUCOSE bare-error-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "insufficient": true, + "text": null, + "updatedAt": null + } + }, + "BLOOD_GLUCOSE blocked-es": { "calls": [ { + "maxTokens": 700, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's blood glucose. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 35 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Cuando la glucosa en sangre es mayor, la frecuencia cardíaca en reposo del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=34, r=0.94)", + "- Cuando la glucosa en sangre es mayor, la presión sistólica del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=23, r=0.91)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"es\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"BLOOD_GLUCOSE\",\"metric\":{\"id\":\"BLOOD_GLUCOSE\",\"displayName\":\"Blood glucose\",\"unit\":\"mmol/L\",\"direction\":\"target-band\",\"normalRange\":{\"low\":3.9,\"high\":7.8}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":35,\"totalSpanDays\":136,\"newestMeasurementDaysAgo\":0},\"BLOOD_GLUCOSE\":{\"signal\":{\"metric\":\"Blood glucose\",\"unit\":\"mmol/L\",\"current\":5.56,\"currentWindowDays\":6,\"baseline\":5.67,\"baselineLabel\":\"your monthly average\",\"delta\":-0.11,\"deltaPct\":-2,\"spread\":0.31,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":6,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.9,\"high\":7.8},\"placement\":\"in band\"},\"summary\":{\"points\":35,\"start\":5.3,\"end\":6,\"delta\":0.7,\"mean\":5.47,\"min\":4.8,\"max\":6},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"date\":\"2026-05-30\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"date\":\"2026-06-03\",\"min\":5.66,\"max\":5.66,\"mean\":5.66,\"n\":1},{\"date\":\"2026-06-07\",\"min\":5.94,\"max\":5.94,\"mean\":5.94,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5.88,\"max\":5.88,\"mean\":5.88,\"n\":1},{\"date\":\"2026-06-15\",\"min\":5.33,\"max\":5.33,\"mean\":5.33,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W13\",\"min\":4.88,\"max\":5.27,\"mean\":5.08,\"n\":2},{\"weekISO\":\"2026-W14\",\"min\":5.77,\"max\":5.83,\"mean\":5.8,\"n\":2},{\"weekISO\":\"2026-W15\",\"min\":5.66,\"max\":5.88,\"mean\":5.77,\"n\":2},{\"weekISO\":\"2026-W16\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W17\",\"min\":4.94,\"max\":5.16,\"mean\":5.05,\"n\":2},{\"weekISO\":\"2026-W18\",\"min\":5.22,\"max\":5.72,\"mean\":5.47,\"n\":2},{\"weekISO\":\"2026-W19\",\"min\":5.72,\"max\":6.05,\"mean\":5.88,\"n\":2},{\"weekISO\":\"2026-W20\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"weekISO\":\"2026-W21\",\"min\":4.83,\"max\":5.22,\"mean\":5.02,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":5.99,\"max\":5.99,\"mean\":5.99,\"n\":1},{\"month\":\"2026-02\",\"min\":4.88,\"max\":5.83,\"mean\":5.37,\"n\":7},{\"month\":\"2026-03\",\"min\":5.22,\"max\":5.99,\"mean\":5.65,\"n\":4}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":5.3}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -849,603 +345,290 @@ "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", "", - "METRIC: Resting heart rate (unit: bpm).", - "Lower values are generally more favourable for this metric.", - "Coarse population reference band: 50–100 bpm (a placement aid only — the user's OWN baseline leads).", + "METRIC: Blood glucose (unit: mmol/L).", + "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", + "Coarse population reference band: 3.9–7.8 mmol/L (a placement aid only — the user's OWN baseline leads).", "", "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with the direction of change over time, then say where it stands now.", - "Write one short assessment of this person's resting heart rate. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 140 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", - "", - "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Cuando la glucosa en sangre es mayor, la frecuencia cardíaca en reposo del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=34, r=0.94)", - "- Cuando la duración del sueño es mayor, la frecuencia cardíaca en reposo del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=89, r=0.86)", - "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", - "", - "", - "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", - "- Current value 60.62 bpm is in the \"the standard resting range\" band, and sits near the LOWER boundary of this band (nearest boundary at 60 bpm).", - "- Guideline bands (AHA 2024; lower values are more favourable):", - " - below 60 bpm: below the standard resting range (common in trained hearts)", - " - 60–100 bpm: the standard resting range", - " - 100 bpm and above: above the standard resting range", - "- Values in the \"the standard resting range\" band are considered a healthy / low-risk range per these guidelines.", - "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", - "", - "", - "{\"locale\":\"es\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.62,\"currentWindowDays\":21,\"baseline\":60.71,\"baselineLabel\":\"your monthly average\",\"delta\":-0.09,\"deltaPct\":-0.1,\"spread\":1.42,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":21,\"newestDaysAgo\":0,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.67,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":58}}}" - ] - } - ], "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "Estable, como de costumbre. Resting heart rate está en 60,6 bpm ahora mismo — por debajo de tu promedio habitual de 60,7 bpm. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "RESTING_HEART_RATE bare-error-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "insufficient": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "RESTING_HEART_RATE bare-error-de": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "insufficient": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "RESTING_HEART_RATE keyless-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Steady and much as usual. Resting heart rate is at 60.6 bpm right now — lower than your usual average of 60.7 bpm. That sits in your usual range — nothing you need to act on right now.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "RESTING_HEART_RATE keyless-de": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Stabil und wie gewohnt. Resting heart rate liegt aktuell bei 60,6 bpm — niedriger als dein üblicher Schnitt von 60,7 bpm. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "RESTING_HEART_RATE keyless-read-only": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "BLOOD_GLUCOSE read-only-cold": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", "metric": "metric:BLOOD_GLUCOSE", - "locale": "en" + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } ], "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "preparing": true, - "revalidating": false - }, + "cached": true, + "hasProvider": false, + "text": "Estable, como de costumbre. Blood glucose está en 5,6 mmol/L ahora mismo — por debajo de tu promedio habitual de 5,7 mmol/L. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", + "updatedAt": null + } + }, + "BLOOD_GLUCOSE cached-en": { + "calls": [], + "enqueued": [], "notes": [ { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "BLOOD_GLUCOSE forced-unchanged-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } }, "BLOOD_GLUCOSE fresh-en": { "calls": [ { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "ARCHETYPE — PHYSIOLOGICAL VITAL:", - "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", - "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", - "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", - "", - "METRIC: Blood glucose (unit: mmol/L).", - "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", - "Coarse population reference band: 3.9–7.8 mmol/L (a placement aid only — the user's OWN baseline leads)." - ], + "maxTokens": 700, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open with the direction of change over time, then say where it stands now.", @@ -1465,248 +648,12 @@ "", "", "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"BLOOD_GLUCOSE\",\"metric\":{\"id\":\"BLOOD_GLUCOSE\",\"displayName\":\"Blood glucose\",\"unit\":\"mmol/L\",\"direction\":\"target-band\",\"normalRange\":{\"low\":3.9,\"high\":7.8}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":35,\"totalSpanDays\":136,\"newestMeasurementDaysAgo\":0},\"BLOOD_GLUCOSE\":{\"signal\":{\"metric\":\"Blood glucose\",\"unit\":\"mmol/L\",\"current\":5.56,\"currentWindowDays\":6,\"baseline\":5.67,\"baselineLabel\":\"your monthly average\",\"delta\":-0.11,\"deltaPct\":-2,\"spread\":0.31,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":6,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.9,\"high\":7.8},\"placement\":\"in band\"},\"summary\":{\"points\":35,\"start\":5.3,\"end\":6,\"delta\":0.7,\"mean\":5.47,\"min\":4.8,\"max\":6},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"date\":\"2026-05-30\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"date\":\"2026-06-03\",\"min\":5.66,\"max\":5.66,\"mean\":5.66,\"n\":1},{\"date\":\"2026-06-07\",\"min\":5.94,\"max\":5.94,\"mean\":5.94,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5.88,\"max\":5.88,\"mean\":5.88,\"n\":1},{\"date\":\"2026-06-15\",\"min\":5.33,\"max\":5.33,\"mean\":5.33,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W13\",\"min\":4.88,\"max\":5.27,\"mean\":5.08,\"n\":2},{\"weekISO\":\"2026-W14\",\"min\":5.77,\"max\":5.83,\"mean\":5.8,\"n\":2},{\"weekISO\":\"2026-W15\",\"min\":5.66,\"max\":5.88,\"mean\":5.77,\"n\":2},{\"weekISO\":\"2026-W16\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W17\",\"min\":4.94,\"max\":5.16,\"mean\":5.05,\"n\":2},{\"weekISO\":\"2026-W18\",\"min\":5.22,\"max\":5.72,\"mean\":5.47,\"n\":2},{\"weekISO\":\"2026-W19\",\"min\":5.72,\"max\":6.05,\"mean\":5.88,\"n\":2},{\"weekISO\":\"2026-W20\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"weekISO\":\"2026-W21\",\"min\":4.83,\"max\":5.22,\"mean\":5.02,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":5.99,\"max\":5.99,\"mean\":5.99,\"n\":1},{\"month\":\"2026-02\",\"min\":4.88,\"max\":5.83,\"mean\":5.37,\"n\":7},{\"month\":\"2026-03\",\"min\":5.22,\"max\":5.99,\"mean\":5.65,\"n\":4}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":5.3}}}" - ] + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "BLOOD_GLUCOSE cached-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "BLOOD_GLUCOSE forced-unchanged-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "BLOOD_GLUCOSE next-day-en": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -1792,9 +739,439 @@ "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", "Coarse population reference band: 3.9–7.8 mmol/L (a placement aid only — the user's OWN baseline leads)." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "BLOOD_GLUCOSE keyless-de": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Stabil und wie gewohnt. Blood glucose liegt aktuell bei 100 mg/dL — niedriger als dein üblicher Schnitt von 102 mg/dL. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", + "updatedAt": null + } + }, + "BLOOD_GLUCOSE keyless-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Steady and much as usual. Blood glucose is at 100 mg/dL right now — lower than your usual average of 102 mg/dL. That sits in your usual range — nothing you need to act on right now.", + "updatedAt": null + } + }, + "BLOOD_GLUCOSE keyless-read-only": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", + "updatedAt": null + } + }, + "BLOOD_GLUCOSE next-day-en": { + "calls": [ + { + "maxTokens": 700, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-16", "OPENER HINT: Open by contrasting now against the user's own baseline.", @@ -1817,179 +1194,207 @@ "", "", "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"BLOOD_GLUCOSE\",\"metric\":{\"id\":\"BLOOD_GLUCOSE\",\"displayName\":\"Blood glucose\",\"unit\":\"mmol/L\",\"direction\":\"target-band\",\"normalRange\":{\"low\":3.9,\"high\":7.8}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":35,\"totalSpanDays\":136,\"newestMeasurementDaysAgo\":1},\"BLOOD_GLUCOSE\":{\"signal\":{\"metric\":\"Blood glucose\",\"unit\":\"mmol/L\",\"current\":5.66,\"currentWindowDays\":5,\"baseline\":5.67,\"baselineLabel\":\"your monthly average\",\"delta\":-0.01,\"deltaPct\":-0.2,\"spread\":0.31,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":5,\"newestDaysAgo\":1,\"normalRange\":{\"low\":3.9,\"high\":7.8},\"placement\":\"in band\"},\"summary\":{\"points\":35,\"start\":5.3,\"end\":6,\"delta\":0.7,\"mean\":5.47,\"min\":4.8,\"max\":6},\"series\":{\"recent\":[{\"date\":\"2026-05-30\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"date\":\"2026-06-03\",\"min\":5.66,\"max\":5.66,\"mean\":5.66,\"n\":1},{\"date\":\"2026-06-07\",\"min\":5.94,\"max\":5.94,\"mean\":5.94,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5.88,\"max\":5.88,\"mean\":5.88,\"n\":1},{\"date\":\"2026-06-15\",\"min\":5.33,\"max\":5.33,\"mean\":5.33,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W13\",\"min\":4.88,\"max\":5.27,\"mean\":5.08,\"n\":2},{\"weekISO\":\"2026-W14\",\"min\":5.77,\"max\":5.83,\"mean\":5.8,\"n\":2},{\"weekISO\":\"2026-W15\",\"min\":5.66,\"max\":5.88,\"mean\":5.77,\"n\":2},{\"weekISO\":\"2026-W16\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W17\",\"min\":4.94,\"max\":5.16,\"mean\":5.05,\"n\":2},{\"weekISO\":\"2026-W18\",\"min\":5.22,\"max\":5.72,\"mean\":5.47,\"n\":2},{\"weekISO\":\"2026-W19\",\"min\":5.72,\"max\":6.05,\"mean\":5.88,\"n\":2},{\"weekISO\":\"2026-W20\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"weekISO\":\"2026-W21\",\"min\":4.83,\"max\":5.22,\"mean\":5.02,\"n\":2},{\"weekISO\":\"2026-W22\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":5.99,\"max\":5.99,\"mean\":5.99,\"n\":1},{\"month\":\"2026-02\",\"min\":4.88,\"max\":5.83,\"mean\":5.37,\"n\":7},{\"month\":\"2026-03\",\"min\":5.22,\"max\":5.99,\"mean\":5.65,\"n\":4}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":5.3}}}" - ] + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — PHYSIOLOGICAL VITAL:", + "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", + "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", + "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", + "", + "METRIC: Blood glucose (unit: mmol/L).", + "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", + "Coarse population reference band: 3.9–7.8 mmol/L (a placement aid only — the user's OWN baseline leads)." + ], + "temperature": 0.45, "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for next-day-en.", - "cached": false, - "updatedAt": "2026-06-16T10:00:00.000Z" - }, "notes": [ { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "BLOOD_GLUCOSE read-only-stale": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } ], "result": { + "cached": false, "hasProvider": true, "text": "Parity note for next-day-en.", - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z", - "preparing": false, - "revalidating": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] + "updatedAt": "2026-06-16T10:00:00.000Z" + } }, "BLOOD_GLUCOSE prose-de": { "calls": [ { + "maxTokens": 700, + "messages": [ + { + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Richtung der Veränderung über die Zeit und nenne dann den aktuellen Stand.", + "Schreibe eine kurze Einschätzung zu Blood glucose dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil und genau da, wo es war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 35 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der Blutzucker höher liegt, scheint die Ruheherzfrequenz am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=34, r=0.94)", + "- Wenn der Blutzucker höher liegt, scheint der systolische Blutdruck am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=23, r=0.91)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"de\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"BLOOD_GLUCOSE\",\"metric\":{\"id\":\"BLOOD_GLUCOSE\",\"displayName\":\"Blood glucose\",\"unit\":\"mmol/L\",\"direction\":\"target-band\",\"normalRange\":{\"low\":3.9,\"high\":7.8}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":35,\"totalSpanDays\":136,\"newestMeasurementDaysAgo\":0},\"BLOOD_GLUCOSE\":{\"signal\":{\"metric\":\"Blood glucose\",\"unit\":\"mmol/L\",\"current\":5.56,\"currentWindowDays\":6,\"baseline\":5.67,\"baselineLabel\":\"your monthly average\",\"delta\":-0.11,\"deltaPct\":-2,\"spread\":0.31,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":6,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.9,\"high\":7.8},\"placement\":\"in band\"},\"summary\":{\"points\":35,\"start\":5.3,\"end\":6,\"delta\":0.7,\"mean\":5.47,\"min\":4.8,\"max\":6},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"date\":\"2026-05-30\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"date\":\"2026-06-03\",\"min\":5.66,\"max\":5.66,\"mean\":5.66,\"n\":1},{\"date\":\"2026-06-07\",\"min\":5.94,\"max\":5.94,\"mean\":5.94,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5.88,\"max\":5.88,\"mean\":5.88,\"n\":1},{\"date\":\"2026-06-15\",\"min\":5.33,\"max\":5.33,\"mean\":5.33,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W13\",\"min\":4.88,\"max\":5.27,\"mean\":5.08,\"n\":2},{\"weekISO\":\"2026-W14\",\"min\":5.77,\"max\":5.83,\"mean\":5.8,\"n\":2},{\"weekISO\":\"2026-W15\",\"min\":5.66,\"max\":5.88,\"mean\":5.77,\"n\":2},{\"weekISO\":\"2026-W16\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W17\",\"min\":4.94,\"max\":5.16,\"mean\":5.05,\"n\":2},{\"weekISO\":\"2026-W18\",\"min\":5.22,\"max\":5.72,\"mean\":5.47,\"n\":2},{\"weekISO\":\"2026-W19\",\"min\":5.72,\"max\":6.05,\"mean\":5.88,\"n\":2},{\"weekISO\":\"2026-W20\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"weekISO\":\"2026-W21\",\"min\":4.83,\"max\":5.22,\"mean\":5.02,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":5.99,\"max\":5.99,\"mean\":5.99,\"n\":1},{\"month\":\"2026-02\",\"min\":4.88,\"max\":5.83,\"mean\":5.37,\"n\":7},{\"month\":\"2026-03\",\"min\":5.22,\"max\":5.99,\"mean\":5.65,\"n\":4}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":5.3}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -2075,15 +1480,267 @@ "Kein Extrem ist gut — der Wert sollte im Normalband liegen; Abweichungen in beide Richtungen sind erwähnenswert.", "Grobes Referenzband (Population): 3.9–7.8 mmol/L (nur Orientierung — die EIGENE Baseline führt)." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Das ist kein JSON.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "BLOOD_GLUCOSE read-only-cold": { + "calls": [], + "enqueued": [ + { + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "userId": "parity-rich" + } + ], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "preparing": true, + "revalidating": false, + "text": null, + "updatedAt": null + } + }, + "BLOOD_GLUCOSE read-only-stale": { + "calls": [], + "enqueued": [ + { + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "userId": "parity-rich" + } + ], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "preparing": false, + "revalidating": true, + "text": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "BLOOD_GLUCOSE unparseable-fr": { + "calls": [ + { + "maxTokens": 700, "messages": [ { - "role": "user", "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne mit der Richtung der Veränderung über die Zeit und nenne dann den aktuellen Stand.", - "Schreibe eine kurze Einschätzung zu Blood glucose dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil und genau da, wo es war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's blood glucose. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", "", "", "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", @@ -2091,113 +1748,18 @@ "DATENLAGE: 35 Tages-Buckets im Fenster, letzter Wert heute.", "", "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Wenn der Blutzucker höher liegt, scheint die Ruheherzfrequenz am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=34, r=0.94)", - "- Wenn der Blutzucker höher liegt, scheint der systolische Blutdruck am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=23, r=0.91)", + "- Quand la glycémie monte, la fréquence cardiaque au repos du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=34, r=0.94)", + "- Quand la glycémie monte, la pression systolique du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=23, r=0.91)", "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", "", "", - "{\"locale\":\"de\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"BLOOD_GLUCOSE\",\"metric\":{\"id\":\"BLOOD_GLUCOSE\",\"displayName\":\"Blood glucose\",\"unit\":\"mmol/L\",\"direction\":\"target-band\",\"normalRange\":{\"low\":3.9,\"high\":7.8}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":35,\"totalSpanDays\":136,\"newestMeasurementDaysAgo\":0},\"BLOOD_GLUCOSE\":{\"signal\":{\"metric\":\"Blood glucose\",\"unit\":\"mmol/L\",\"current\":5.56,\"currentWindowDays\":6,\"baseline\":5.67,\"baselineLabel\":\"your monthly average\",\"delta\":-0.11,\"deltaPct\":-2,\"spread\":0.31,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":6,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.9,\"high\":7.8},\"placement\":\"in band\"},\"summary\":{\"points\":35,\"start\":5.3,\"end\":6,\"delta\":0.7,\"mean\":5.47,\"min\":4.8,\"max\":6},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"date\":\"2026-05-30\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"date\":\"2026-06-03\",\"min\":5.66,\"max\":5.66,\"mean\":5.66,\"n\":1},{\"date\":\"2026-06-07\",\"min\":5.94,\"max\":5.94,\"mean\":5.94,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5.88,\"max\":5.88,\"mean\":5.88,\"n\":1},{\"date\":\"2026-06-15\",\"min\":5.33,\"max\":5.33,\"mean\":5.33,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W13\",\"min\":4.88,\"max\":5.27,\"mean\":5.08,\"n\":2},{\"weekISO\":\"2026-W14\",\"min\":5.77,\"max\":5.83,\"mean\":5.8,\"n\":2},{\"weekISO\":\"2026-W15\",\"min\":5.66,\"max\":5.88,\"mean\":5.77,\"n\":2},{\"weekISO\":\"2026-W16\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W17\",\"min\":4.94,\"max\":5.16,\"mean\":5.05,\"n\":2},{\"weekISO\":\"2026-W18\",\"min\":5.22,\"max\":5.72,\"mean\":5.47,\"n\":2},{\"weekISO\":\"2026-W19\",\"min\":5.72,\"max\":6.05,\"mean\":5.88,\"n\":2},{\"weekISO\":\"2026-W20\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"weekISO\":\"2026-W21\",\"min\":4.83,\"max\":5.22,\"mean\":5.02,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":5.99,\"max\":5.99,\"mean\":5.99,\"n\":1},{\"month\":\"2026-02\",\"min\":4.88,\"max\":5.83,\"mean\":5.37,\"n\":7},{\"month\":\"2026-03\",\"min\":5.22,\"max\":5.99,\"mean\":5.65,\"n\":4}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":5.3}}}" - ] + "{\"locale\":\"fr\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"BLOOD_GLUCOSE\",\"metric\":{\"id\":\"BLOOD_GLUCOSE\",\"displayName\":\"Blood glucose\",\"unit\":\"mmol/L\",\"direction\":\"target-band\",\"normalRange\":{\"low\":3.9,\"high\":7.8}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":35,\"totalSpanDays\":136,\"newestMeasurementDaysAgo\":0},\"BLOOD_GLUCOSE\":{\"signal\":{\"metric\":\"Blood glucose\",\"unit\":\"mmol/L\",\"current\":5.56,\"currentWindowDays\":6,\"baseline\":5.67,\"baselineLabel\":\"your monthly average\",\"delta\":-0.11,\"deltaPct\":-2,\"spread\":0.31,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":6,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.9,\"high\":7.8},\"placement\":\"in band\"},\"summary\":{\"points\":35,\"start\":5.3,\"end\":6,\"delta\":0.7,\"mean\":5.47,\"min\":4.8,\"max\":6},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"date\":\"2026-05-30\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"date\":\"2026-06-03\",\"min\":5.66,\"max\":5.66,\"mean\":5.66,\"n\":1},{\"date\":\"2026-06-07\",\"min\":5.94,\"max\":5.94,\"mean\":5.94,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5.88,\"max\":5.88,\"mean\":5.88,\"n\":1},{\"date\":\"2026-06-15\",\"min\":5.33,\"max\":5.33,\"mean\":5.33,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W13\",\"min\":4.88,\"max\":5.27,\"mean\":5.08,\"n\":2},{\"weekISO\":\"2026-W14\",\"min\":5.77,\"max\":5.83,\"mean\":5.8,\"n\":2},{\"weekISO\":\"2026-W15\",\"min\":5.66,\"max\":5.88,\"mean\":5.77,\"n\":2},{\"weekISO\":\"2026-W16\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W17\",\"min\":4.94,\"max\":5.16,\"mean\":5.05,\"n\":2},{\"weekISO\":\"2026-W18\",\"min\":5.22,\"max\":5.72,\"mean\":5.47,\"n\":2},{\"weekISO\":\"2026-W19\",\"min\":5.72,\"max\":6.05,\"mean\":5.88,\"n\":2},{\"weekISO\":\"2026-W20\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"weekISO\":\"2026-W21\",\"min\":4.83,\"max\":5.22,\"mean\":5.02,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":5.99,\"max\":5.99,\"mean\":5.99,\"n\":1},{\"month\":\"2026-02\",\"min\":4.88,\"max\":5.83,\"mean\":5.37,\"n\":7},{\"month\":\"2026-03\",\"min\":5.22,\"max\":5.99,\"mean\":5.65,\"n\":4}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":5.3}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Das ist kein JSON.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "BLOOD_GLUCOSE unparseable-fr": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -2285,142 +1847,284 @@ "", "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Stable, comme d’habitude. Blood glucose est à 5,6 mmol/L en ce moment — en dessous de ta moyenne habituelle de 5,7 mmol/L. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", + "updatedAt": null + } + }, + "RESTING_HEART_RATE bare-error-de": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "insufficient": true, + "text": null, + "updatedAt": null + } + }, + "RESTING_HEART_RATE bare-error-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "insufficient": true, + "text": null, + "updatedAt": null + } + }, + "RESTING_HEART_RATE blocked-es": { + "calls": [ + { + "maxTokens": 700, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open with the direction of change over time, then say where it stands now.", - "Write one short assessment of this person's blood glucose. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "Write one short assessment of this person's resting heart rate. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", "", "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", "", "", "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", "", - "DATENLAGE: 35 Tages-Buckets im Fenster, letzter Wert heute.", + "DATENLAGE: 140 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", "", "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Quand la glycémie monte, la fréquence cardiaque au repos du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=34, r=0.94)", - "- Quand la glycémie monte, la pression systolique du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=23, r=0.91)", + "- Cuando la glucosa en sangre es mayor, la frecuencia cardíaca en reposo del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=34, r=0.94)", + "- Cuando la duración del sueño es mayor, la frecuencia cardíaca en reposo del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=89, r=0.86)", "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", "", "", - "{\"locale\":\"fr\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"BLOOD_GLUCOSE\",\"metric\":{\"id\":\"BLOOD_GLUCOSE\",\"displayName\":\"Blood glucose\",\"unit\":\"mmol/L\",\"direction\":\"target-band\",\"normalRange\":{\"low\":3.9,\"high\":7.8}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":35,\"totalSpanDays\":136,\"newestMeasurementDaysAgo\":0},\"BLOOD_GLUCOSE\":{\"signal\":{\"metric\":\"Blood glucose\",\"unit\":\"mmol/L\",\"current\":5.56,\"currentWindowDays\":6,\"baseline\":5.67,\"baselineLabel\":\"your monthly average\",\"delta\":-0.11,\"deltaPct\":-2,\"spread\":0.31,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":6,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.9,\"high\":7.8},\"placement\":\"in band\"},\"summary\":{\"points\":35,\"start\":5.3,\"end\":6,\"delta\":0.7,\"mean\":5.47,\"min\":4.8,\"max\":6},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"date\":\"2026-05-30\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"date\":\"2026-06-03\",\"min\":5.66,\"max\":5.66,\"mean\":5.66,\"n\":1},{\"date\":\"2026-06-07\",\"min\":5.94,\"max\":5.94,\"mean\":5.94,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5.88,\"max\":5.88,\"mean\":5.88,\"n\":1},{\"date\":\"2026-06-15\",\"min\":5.33,\"max\":5.33,\"mean\":5.33,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W13\",\"min\":4.88,\"max\":5.27,\"mean\":5.08,\"n\":2},{\"weekISO\":\"2026-W14\",\"min\":5.77,\"max\":5.83,\"mean\":5.8,\"n\":2},{\"weekISO\":\"2026-W15\",\"min\":5.66,\"max\":5.88,\"mean\":5.77,\"n\":2},{\"weekISO\":\"2026-W16\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W17\",\"min\":4.94,\"max\":5.16,\"mean\":5.05,\"n\":2},{\"weekISO\":\"2026-W18\",\"min\":5.22,\"max\":5.72,\"mean\":5.47,\"n\":2},{\"weekISO\":\"2026-W19\",\"min\":5.72,\"max\":6.05,\"mean\":5.88,\"n\":2},{\"weekISO\":\"2026-W20\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"weekISO\":\"2026-W21\",\"min\":4.83,\"max\":5.22,\"mean\":5.02,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":5.99,\"max\":5.99,\"mean\":5.99,\"n\":1},{\"month\":\"2026-02\",\"min\":4.88,\"max\":5.83,\"mean\":5.37,\"n\":7},{\"month\":\"2026-03\",\"min\":5.22,\"max\":5.99,\"mean\":5.65,\"n\":4}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":5.3}}}" - ] + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 60.62 bpm is in the \"the standard resting range\" band, and sits near the LOWER boundary of this band (nearest boundary at 60 bpm).", + "- Guideline bands (AHA 2024; lower values are more favourable):", + " - below 60 bpm: below the standard resting range (common in trained hearts)", + " - 60–100 bpm: the standard resting range", + " - 100 bpm and above: above the standard resting range", + "- Values in the \"the standard resting range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", + "", + "", + "{\"locale\":\"es\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.62,\"currentWindowDays\":21,\"baseline\":60.71,\"baselineLabel\":\"your monthly average\",\"delta\":-0.09,\"deltaPct\":-0.1,\"spread\":1.42,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":21,\"newestDaysAgo\":0,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.67,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":58}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Stable, comme d’habitude. Blood glucose est à 5,6 mmol/L en ce moment — en dessous de ta moyenne habituelle de 5,7 mmol/L. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "BLOOD_GLUCOSE blocked-es": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -2502,867 +2206,171 @@ "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", "", - "METRIC: Blood glucose (unit: mmol/L).", - "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", - "Coarse population reference band: 3.9–7.8 mmol/L (a placement aid only — the user's OWN baseline leads).", + "METRIC: Resting heart rate (unit: bpm).", + "Lower values are generally more favourable for this metric.", + "Coarse population reference band: 50–100 bpm (a placement aid only — the user's OWN baseline leads).", "", "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with the direction of change over time, then say where it stands now.", - "Write one short assessment of this person's blood glucose. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 35 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Cuando la glucosa en sangre es mayor, la frecuencia cardíaca en reposo del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=34, r=0.94)", - "- Cuando la glucosa en sangre es mayor, la presión sistólica del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=23, r=0.91)", - "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", - "", - "", - "{\"locale\":\"es\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"BLOOD_GLUCOSE\",\"metric\":{\"id\":\"BLOOD_GLUCOSE\",\"displayName\":\"Blood glucose\",\"unit\":\"mmol/L\",\"direction\":\"target-band\",\"normalRange\":{\"low\":3.9,\"high\":7.8}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":35,\"totalSpanDays\":136,\"newestMeasurementDaysAgo\":0},\"BLOOD_GLUCOSE\":{\"signal\":{\"metric\":\"Blood glucose\",\"unit\":\"mmol/L\",\"current\":5.56,\"currentWindowDays\":6,\"baseline\":5.67,\"baselineLabel\":\"your monthly average\",\"delta\":-0.11,\"deltaPct\":-2,\"spread\":0.31,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":6,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.9,\"high\":7.8},\"placement\":\"in band\"},\"summary\":{\"points\":35,\"start\":5.3,\"end\":6,\"delta\":0.7,\"mean\":5.47,\"min\":4.8,\"max\":6},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"date\":\"2026-05-30\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"date\":\"2026-06-03\",\"min\":5.66,\"max\":5.66,\"mean\":5.66,\"n\":1},{\"date\":\"2026-06-07\",\"min\":5.94,\"max\":5.94,\"mean\":5.94,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5.88,\"max\":5.88,\"mean\":5.88,\"n\":1},{\"date\":\"2026-06-15\",\"min\":5.33,\"max\":5.33,\"mean\":5.33,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W13\",\"min\":4.88,\"max\":5.27,\"mean\":5.08,\"n\":2},{\"weekISO\":\"2026-W14\",\"min\":5.77,\"max\":5.83,\"mean\":5.8,\"n\":2},{\"weekISO\":\"2026-W15\",\"min\":5.66,\"max\":5.88,\"mean\":5.77,\"n\":2},{\"weekISO\":\"2026-W16\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W17\",\"min\":4.94,\"max\":5.16,\"mean\":5.05,\"n\":2},{\"weekISO\":\"2026-W18\",\"min\":5.22,\"max\":5.72,\"mean\":5.47,\"n\":2},{\"weekISO\":\"2026-W19\",\"min\":5.72,\"max\":6.05,\"mean\":5.88,\"n\":2},{\"weekISO\":\"2026-W20\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"weekISO\":\"2026-W21\",\"min\":4.83,\"max\":5.22,\"mean\":5.02,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":5.99,\"max\":5.99,\"mean\":5.99,\"n\":1},{\"month\":\"2026-02\",\"min\":4.88,\"max\":5.83,\"mean\":5.37,\"n\":7},{\"month\":\"2026-03\",\"min\":5.22,\"max\":5.99,\"mean\":5.65,\"n\":4}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":5.3}}}" - ] - } - ], "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "Estable, como de costumbre. Blood glucose está en 5,6 mmol/L ahora mismo — por debajo de tu promedio habitual de 5,7 mmol/L. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "BLOOD_GLUCOSE bare-error-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "insufficient": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "BLOOD_GLUCOSE bare-error-de": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "insufficient": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "BLOOD_GLUCOSE keyless-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Steady and much as usual. Blood glucose is at 100 mg/dL right now — lower than your usual average of 102 mg/dL. That sits in your usual range — nothing you need to act on right now.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "BLOOD_GLUCOSE keyless-de": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Stabil und wie gewohnt. Blood glucose liegt aktuell bei 100 mg/dL — niedriger als dein üblicher Schnitt von 102 mg/dL. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "BLOOD_GLUCOSE keyless-read-only": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "SLEEP_DURATION read-only-cold": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en" + "userId": "parity-rich" } ], "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "preparing": true, - "revalidating": false - }, + "cached": true, + "hasProvider": false, + "text": "Estable, como de costumbre. Resting heart rate está en 60,6 bpm ahora mismo — por debajo de tu promedio habitual de 60,7 bpm. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", + "updatedAt": null + } + }, + "RESTING_HEART_RATE cached-en": { + "calls": [], + "enqueued": [], "notes": [ { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", + "inputHash": null, + "items": null, "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, + "negativeReason": null, + "retryAt": null, "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "RESTING_HEART_RATE forced-unchanged-en": { + "calls": [], + "enqueued": [], + "notes": [ + { "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", + "inputHash": null, + "items": null, "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } }, - "SLEEP_DURATION fresh-en": { + "RESTING_HEART_RATE fresh-en": { "calls": [ { + "maxTokens": 700, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's resting heart rate. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 140 day-buckets in the window, latest reading today.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 11 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 12 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher blood glucose looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=34, r=0.94)", + "- Higher sleep duration looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=89, r=0.86)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 60.62 bpm is in the \"the standard resting range\" band, and sits near the LOWER boundary of this band (nearest boundary at 60 bpm).", + "- Guideline bands (AHA 2024; lower values are more favourable):", + " - below 60 bpm: below the standard resting range (common in trained hearts)", + " - 60–100 bpm: the standard resting range", + " - 100 bpm and above: above the standard resting range", + "- Values in the \"the standard resting range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", + "", + "", + "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.62,\"currentWindowDays\":21,\"baseline\":60.71,\"baselineLabel\":\"your monthly average\",\"delta\":-0.09,\"deltaPct\":-0.1,\"spread\":1.42,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":21,\"newestDaysAgo\":0,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.67,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":58}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -3439,6 +2447,1252 @@ "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", "{ \"summary\": \"...\" }", "", + "ARCHETYPE — PHYSIOLOGICAL VITAL:", + "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", + "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", + "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", + "", + "METRIC: Resting heart rate (unit: bpm).", + "Lower values are generally more favourable for this metric.", + "Coarse population reference band: 50–100 bpm (a placement aid only — the user's OWN baseline leads)." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "RESTING_HEART_RATE keyless-de": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Stabil und wie gewohnt. Resting heart rate liegt aktuell bei 60,6 bpm — niedriger als dein üblicher Schnitt von 60,7 bpm. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", + "updatedAt": null + } + }, + "RESTING_HEART_RATE keyless-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Steady and much as usual. Resting heart rate is at 60.6 bpm right now — lower than your usual average of 60.7 bpm. That sits in your usual range — nothing you need to act on right now.", + "updatedAt": null + } + }, + "RESTING_HEART_RATE keyless-read-only": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", + "updatedAt": null + } + }, + "RESTING_HEART_RATE next-day-en": { + "calls": [ + { + "maxTokens": 700, + "messages": [ + { + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's resting heart rate. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 140 day-buckets in the window, latest reading 1 day ago.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 11 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 12 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher blood glucose looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=34, r=0.94)", + "- Higher sleep duration looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=89, r=0.86)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 60.85 bpm is in the \"the standard resting range\" band, and sits near the LOWER boundary of this band (nearest boundary at 60 bpm).", + "- Guideline bands (AHA 2024; lower values are more favourable):", + " - below 60 bpm: below the standard resting range (common in trained hearts)", + " - 60–100 bpm: the standard resting range", + " - 100 bpm and above: above the standard resting range", + "- Values in the \"the standard resting range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", + "", + "", + "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":1},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.85,\"currentWindowDays\":20,\"baseline\":60.65,\"baselineLabel\":\"your monthly average\",\"delta\":0.2,\"deltaPct\":0.3,\"spread\":1.41,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.4,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.76,\"n\":17}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":58}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — PHYSIOLOGICAL VITAL:", + "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", + "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", + "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", + "", + "METRIC: Resting heart rate (unit: bpm).", + "Lower values are generally more favourable for this metric.", + "Coarse population reference band: 50–100 bpm (a placement aid only — the user's OWN baseline leads)." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "RESTING_HEART_RATE prose-de": { + "calls": [ + { + "maxTokens": 700, + "messages": [ + { + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Richtung der Veränderung über die Zeit und nenne dann den aktuellen Stand.", + "Schreibe eine kurze Einschätzung zu Resting heart rate dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil und genau da, wo es war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 140 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der Blutzucker höher liegt, scheint die Ruheherzfrequenz am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=34, r=0.94)", + "- Wenn die Schlafdauer höher liegt, scheint die Ruheherzfrequenz am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=89, r=0.86)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "EINORDNUNGS-KONTEXT (Leitlinien-Bänder — serverseitig berechnet; nennen, NICHT neu rechnen):", + "- Der aktuelle Wert 60.62 bpm liegt im Band \"the standard resting range\" und liegt nahe der UNTEREN Grenze dieses Bands (nächste Grenze bei 60 bpm).", + "- Leitlinien-Bänder (AHA 2024; niedrigere Werte sind günstiger):", + " - unter 60 bpm: below the standard resting range (common in trained hearts)", + " - 60–100 bpm: the standard resting range", + " - ab 100 bpm: above the standard resting range", + "- Werte im Band \"the standard resting range\" gelten laut diesen Leitlinien als ein gesunder / risikoarmer Bereich.", + "- Benenne, wo der Wert liegt und was dieses Band praktisch bedeutet. Beurteile jeden Trend GEMÄSS dieser Position: eine Verschiebung tief in einem günstigen Band ist eine Randnotiz; dieselbe Verschiebung nahe an oder über einer Grenze ist die Schlagzeile. Rahme die Konsequenz ohne Diagnose (\"Werte in diesem Bereich gelten als …\", nie \"du hast / bist gefährdet für …\").", + "", + "", + "{\"locale\":\"de\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.62,\"currentWindowDays\":21,\"baseline\":60.71,\"baselineLabel\":\"your monthly average\",\"delta\":-0.09,\"deltaPct\":-0.1,\"spread\":1.42,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":21,\"newestDaysAgo\":0,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.67,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":58}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYP — PHYSIOLOGISCHER VITALWERT:", + "- Das ist ein Momentanwert. Benenne das aktuelle Niveau und ordne es sowohl gegen die eigene Baseline (recent vs. Monats-/Jahresmittel) als auch — falls ein Referenzband vorliegt — gegen dieses Band ein.", + "- Ein einzelner Wert außerhalb des Bands ist selten ein Befund; eine anhaltende Verschiebung über das recent-Fenster gegen die Baseline schon. Erwähne einen Trend außerhalb des Bands sachlich, ohne zu alarmieren.", + "- Formuliere jede Richtung (steigend/fallend) als mögliche Erklärung (Fitness, Stress, Schlaf, Krankheit, Medikament), nie als Diagnose.", + "", + "METRIK: Resting heart rate (Einheit: bpm).", + "Niedrigere Werte sind bei dieser Metrik tendenziell günstiger.", + "Grobes Referenzband (Population): 50–100 bpm (nur Orientierung — die EIGENE Baseline führt)." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Das ist kein JSON.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "RESTING_HEART_RATE read-only-cold": { + "calls": [], + "enqueued": [ + { + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "userId": "parity-rich" + } + ], + "notes": [], + "result": { + "cached": false, + "hasProvider": true, + "preparing": true, + "revalidating": false, + "text": null, + "updatedAt": null + } + }, + "RESTING_HEART_RATE read-only-stale": { + "calls": [], + "enqueued": [ + { + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "userId": "parity-rich" + } + ], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "preparing": false, + "revalidating": true, + "text": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "RESTING_HEART_RATE unparseable-fr": { + "calls": [ + { + "maxTokens": 700, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's resting heart rate. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 140 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Quand la glycémie monte, la fréquence cardiaque au repos du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=34, r=0.94)", + "- Quand la durée de sommeil monte, la fréquence cardiaque au repos du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=89, r=0.86)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 60.62 bpm is in the \"the standard resting range\" band, and sits near the LOWER boundary of this band (nearest boundary at 60 bpm).", + "- Guideline bands (AHA 2024; lower values are more favourable):", + " - below 60 bpm: below the standard resting range (common in trained hearts)", + " - 60–100 bpm: the standard resting range", + " - 100 bpm and above: above the standard resting range", + "- Values in the \"the standard resting range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", + "", + "", + "{\"locale\":\"fr\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.62,\"currentWindowDays\":21,\"baseline\":60.71,\"baselineLabel\":\"your monthly average\",\"delta\":-0.09,\"deltaPct\":-0.1,\"spread\":1.42,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":21,\"newestDaysAgo\":0,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.67,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":58}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — PHYSIOLOGICAL VITAL:", + "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", + "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", + "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", + "", + "METRIC: Resting heart rate (unit: bpm).", + "Lower values are generally more favourable for this metric.", + "Coarse population reference band: 50–100 bpm (a placement aid only — the user's OWN baseline leads).", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Stable, comme d’habitude. Resting heart rate est à 60,6 bpm en ce moment — en dessous de ta moyenne habituelle de 60,7 bpm. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", + "updatedAt": null + } + }, + "SLEEP_DURATION bare-error-de": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "insufficient": true, + "text": null, + "updatedAt": null + } + }, + "SLEEP_DURATION bare-error-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "insufficient": true, + "text": null, + "updatedAt": null + } + }, + "SLEEP_DURATION blocked-es": { + "calls": [ + { + "maxTokens": 700, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's sleep duration. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 89 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 3-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 4 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Cuando la duración del sueño es mayor, el peso del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=45, r=0.98)", + "- Cuando la glucosa en sangre es mayor, la duración del sueño del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=22, r=0.95)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 423.85 min is in the \"the recommended adult range\" band, and sits near the LOWER boundary of this band (nearest boundary at 420 min).", + "- Guideline bands (NSF 2015; AASM/SRS 2015; the middle band is the target — both extremes are less favourable):", + " - below 420 min: below the recommended adult range", + " - 420–540 min: the recommended adult range", + " - 540 min and above: above the recommended adult range", + "- Values in the \"the recommended adult range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", + "", + "", + "{\"locale\":\"es\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"SLEEP_DURATION\",\"metric\":{\"id\":\"SLEEP_DURATION\",\"displayName\":\"Sleep duration\",\"unit\":\"min\",\"direction\":\"target-band\",\"normalRange\":{\"low\":420,\"high\":540}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":89,\"totalSpanDays\":88,\"newestMeasurementDaysAgo\":1},\"SLEEP_DURATION\":{\"signal\":{\"metric\":\"Sleep duration\",\"unit\":\"min\",\"current\":423.85,\"currentWindowDays\":20,\"baseline\":404.11,\"baselineLabel\":\"your recent-weeks average\",\"delta\":19.74,\"deltaPct\":4.9,\"spread\":25.3,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":420,\"high\":540},\"placement\":\"in band\"},\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":375,\"max\":380,\"mean\":376.6,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":379,\"max\":414,\"mean\":394.86,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":421,\"max\":445,\"mean\":435.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":418,\"max\":445,\"mean\":434,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":378,\"max\":411,\"mean\":392.57,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":375,\"max\":391,\"mean\":380.43,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":397,\"max\":436,\"mean\":417.14,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":438,\"max\":445,\"mean\":442.29,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":394,\"max\":433,\"mean\":413.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":375,\"max\":388,\"mean\":379,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":379,\"max\":379,\"mean\":379,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":417}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in Spanish: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", "ARCHETYPE — SLEEP:", "- This is nightly sleep duration. The snapshot's values are in minutes; convert to hours for your prose (e.g. 450 min = 7.5 h). Assess both the typical duration and its CONSISTENCY: a stable ~7–9 h is the favourable pattern; high night-to-night variability is itself worth naming even when the mean looks fine.", "- Compare the recent nights to the user's own weekly/monthly baseline. A reference band of 7–9 h is a coarse adult guide, not a verdict — short or long sleepers vary.", @@ -3446,11 +3700,444 @@ "", "METRIC: Sleep duration (unit: min).", "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", - "Coarse population reference band: 420–540 min (a placement aid only — the user's OWN baseline leads)." + "Coarse population reference band: 420–540 min (a placement aid only — the user's OWN baseline leads).", + "", + "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Estable, como de costumbre. Sleep duration está en 423,9 min ahora mismo — por encima de tu promedio habitual de 404,1 min. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", + "updatedAt": null + } + }, + "SLEEP_DURATION cached-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "SLEEP_DURATION forced-unchanged-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "SLEEP_DURATION fresh-en": { + "calls": [ + { + "maxTokens": 700, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open with what this means for the user today, then the number behind it.", @@ -3482,404 +4169,12 @@ "", "", "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"SLEEP_DURATION\",\"metric\":{\"id\":\"SLEEP_DURATION\",\"displayName\":\"Sleep duration\",\"unit\":\"min\",\"direction\":\"target-band\",\"normalRange\":{\"low\":420,\"high\":540}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":89,\"totalSpanDays\":88,\"newestMeasurementDaysAgo\":1},\"SLEEP_DURATION\":{\"signal\":{\"metric\":\"Sleep duration\",\"unit\":\"min\",\"current\":423.85,\"currentWindowDays\":20,\"baseline\":404.11,\"baselineLabel\":\"your recent-weeks average\",\"delta\":19.74,\"deltaPct\":4.9,\"spread\":25.3,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":420,\"high\":540},\"placement\":\"in band\"},\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":375,\"max\":380,\"mean\":376.6,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":379,\"max\":414,\"mean\":394.86,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":421,\"max\":445,\"mean\":435.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":418,\"max\":445,\"mean\":434,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":378,\"max\":411,\"mean\":392.57,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":375,\"max\":391,\"mean\":380.43,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":397,\"max\":436,\"mean\":417.14,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":438,\"max\":445,\"mean\":442.29,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":394,\"max\":433,\"mean\":413.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":375,\"max\":388,\"mean\":379,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":379,\"max\":379,\"mean\":379,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":417}}}" - ] + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "SLEEP_DURATION cached-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "SLEEP_DURATION forced-unchanged-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "SLEEP_DURATION next-day-en": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -3965,9 +4260,647 @@ "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", "Coarse population reference band: 420–540 min (a placement aid only — the user's OWN baseline leads)." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "SLEEP_DURATION keyless-de": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Stabil und wie gewohnt. Sleep duration liegt aktuell bei 423,9 min — höher als dein üblicher Schnitt von 404,1 min. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", + "updatedAt": null + } + }, + "SLEEP_DURATION keyless-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Steady and much as usual. Sleep duration is at 423.9 min right now — higher than your usual average of 404.1 min. That sits in your usual range — nothing you need to act on right now.", + "updatedAt": null + } + }, + "SLEEP_DURATION keyless-read-only": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", + "updatedAt": null + } + }, + "SLEEP_DURATION next-day-en": { + "calls": [ + { + "maxTokens": 700, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-16", "OPENER HINT: Open by contrasting now against the user's own baseline.", @@ -4002,283 +4935,271 @@ "", "", "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"SLEEP_DURATION\",\"metric\":{\"id\":\"SLEEP_DURATION\",\"displayName\":\"Sleep duration\",\"unit\":\"min\",\"direction\":\"target-band\",\"normalRange\":{\"low\":420,\"high\":540}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":89,\"totalSpanDays\":88,\"newestMeasurementDaysAgo\":2},\"SLEEP_DURATION\":{\"signal\":{\"metric\":\"Sleep duration\",\"unit\":\"min\",\"current\":425.95,\"currentWindowDays\":19,\"baseline\":404.33,\"baselineLabel\":\"your recent-weeks average\",\"delta\":21.61,\"deltaPct\":5.3,\"spread\":25.06,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":19,\"newestDaysAgo\":2,\"normalRange\":{\"low\":420,\"high\":540},\"placement\":\"in band\"},\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":375,\"max\":380,\"mean\":376.6,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":379,\"max\":414,\"mean\":394.86,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":421,\"max\":445,\"mean\":435.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":418,\"max\":445,\"mean\":434,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":378,\"max\":411,\"mean\":392.57,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":375,\"max\":391,\"mean\":380.43,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":397,\"max\":436,\"mean\":417.14,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":438,\"max\":445,\"mean\":442.29,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":394,\"max\":433,\"mean\":413.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":375,\"max\":388,\"mean\":379,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":379,\"max\":384,\"mean\":381.5,\"n\":2}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":2,\"value\":417}}}" - ] + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — SLEEP:", + "- This is nightly sleep duration. The snapshot's values are in minutes; convert to hours for your prose (e.g. 450 min = 7.5 h). Assess both the typical duration and its CONSISTENCY: a stable ~7–9 h is the favourable pattern; high night-to-night variability is itself worth naming even when the mean looks fine.", + "- Compare the recent nights to the user's own weekly/monthly baseline. A reference band of 7–9 h is a coarse adult guide, not a verdict — short or long sleepers vary.", + "- Close with one doable step toward more consistent or sufficient sleep ONLY when the finding implies one — never the forbidden 'get enough sleep' platitude; be specific (e.g. a steadier wind-down time). When sleep is already consistent and sufficient, affirm it and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "METRIC: Sleep duration (unit: min).", + "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", + "Coarse population reference band: 420–540 min (a placement aid only — the user's OWN baseline leads)." + ], + "temperature": 0.45, "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for next-day-en.", - "cached": false, - "updatedAt": "2026-06-16T10:00:00.000Z" - }, "notes": [ { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", "text": "Das ist kein JSON.", - "items": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", "text": "Parity note for next-day-en.", - "items": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, "inputHash": null, - "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "SLEEP_DURATION read-only-stale": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", + "inputHash": null, + "items": null, + "locale": "en", "metric": "metric:SLEEP_DURATION", - "locale": "en" + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" } ], "result": { + "cached": false, "hasProvider": true, "text": "Parity note for next-day-en.", - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z", - "preparing": false, - "revalidating": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] + "updatedAt": "2026-06-16T10:00:00.000Z" + } }, "SLEEP_DURATION prose-de": { "calls": [ { + "maxTokens": 700, + "messages": [ + { + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zu Sleep duration dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil und genau da, wo es war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 89 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 3-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 4 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn die Schlafdauer höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=45, r=0.98)", + "- Wenn der Blutzucker höher liegt, scheint die Schlafdauer am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=22, r=0.95)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "EINORDNUNGS-KONTEXT (Leitlinien-Bänder — serverseitig berechnet; nennen, NICHT neu rechnen):", + "- Der aktuelle Wert 423.85 min liegt im Band \"the recommended adult range\" und liegt nahe der UNTEREN Grenze dieses Bands (nächste Grenze bei 420 min).", + "- Leitlinien-Bänder (NSF 2015; AASM/SRS 2015; das mittlere Band ist das Ziel — beide Extreme sind ungünstiger):", + " - unter 420 min: below the recommended adult range", + " - 420–540 min: the recommended adult range", + " - ab 540 min: above the recommended adult range", + "- Werte im Band \"the recommended adult range\" gelten laut diesen Leitlinien als ein gesunder / risikoarmer Bereich.", + "- Benenne, wo der Wert liegt und was dieses Band praktisch bedeutet. Beurteile jeden Trend GEMÄSS dieser Position: eine Verschiebung tief in einem günstigen Band ist eine Randnotiz; dieselbe Verschiebung nahe an oder über einer Grenze ist die Schlagzeile. Rahme die Konsequenz ohne Diagnose (\"Werte in diesem Bereich gelten als …\", nie \"du hast / bist gefährdet für …\").", + "", + "", + "{\"locale\":\"de\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"SLEEP_DURATION\",\"metric\":{\"id\":\"SLEEP_DURATION\",\"displayName\":\"Sleep duration\",\"unit\":\"min\",\"direction\":\"target-band\",\"normalRange\":{\"low\":420,\"high\":540}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":89,\"totalSpanDays\":88,\"newestMeasurementDaysAgo\":1},\"SLEEP_DURATION\":{\"signal\":{\"metric\":\"Sleep duration\",\"unit\":\"min\",\"current\":423.85,\"currentWindowDays\":20,\"baseline\":404.11,\"baselineLabel\":\"your recent-weeks average\",\"delta\":19.74,\"deltaPct\":4.9,\"spread\":25.3,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":420,\"high\":540},\"placement\":\"in band\"},\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":375,\"max\":380,\"mean\":376.6,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":379,\"max\":414,\"mean\":394.86,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":421,\"max\":445,\"mean\":435.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":418,\"max\":445,\"mean\":434,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":378,\"max\":411,\"mean\":392.57,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":375,\"max\":391,\"mean\":380.43,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":397,\"max\":436,\"mean\":417.14,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":438,\"max\":445,\"mean\":442.29,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":394,\"max\":433,\"mean\":413.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":375,\"max\":388,\"mean\":379,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":379,\"max\":379,\"mean\":379,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":417}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -4364,15 +5285,423 @@ "Kein Extrem ist gut — der Wert sollte im Normalband liegen; Abweichungen in beide Richtungen sind erwähnenswert.", "Grobes Referenzband (Population): 420–540 min (nur Orientierung — die EIGENE Baseline führt)." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Das ist kein JSON.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "SLEEP_DURATION read-only-cold": { + "calls": [], + "enqueued": [ + { + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "userId": "parity-rich" + } + ], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "preparing": true, + "revalidating": false, + "text": null, + "updatedAt": null + } + }, + "SLEEP_DURATION read-only-stale": { + "calls": [], + "enqueued": [ + { + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "userId": "parity-rich" + } + ], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "preparing": false, + "revalidating": true, + "text": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "SLEEP_DURATION unparseable-fr": { + "calls": [ + { + "maxTokens": 700, "messages": [ { - "role": "user", "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", - "Schreibe eine kurze Einschätzung zu Sleep duration dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil und genau da, wo es war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's sleep duration. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", "", "", "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", @@ -4382,175 +5711,28 @@ "WIEDERHOLUNG: du hast für diese Metrik bereits 3-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 4 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", "", "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Wenn die Schlafdauer höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=45, r=0.98)", - "- Wenn der Blutzucker höher liegt, scheint die Schlafdauer am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=22, r=0.95)", + "- Quand la durée de sommeil monte, le poids du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=45, r=0.98)", + "- Quand la glycémie monte, la durée de sommeil du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=22, r=0.95)", "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", "", "", - "EINORDNUNGS-KONTEXT (Leitlinien-Bänder — serverseitig berechnet; nennen, NICHT neu rechnen):", - "- Der aktuelle Wert 423.85 min liegt im Band \"the recommended adult range\" und liegt nahe der UNTEREN Grenze dieses Bands (nächste Grenze bei 420 min).", - "- Leitlinien-Bänder (NSF 2015; AASM/SRS 2015; das mittlere Band ist das Ziel — beide Extreme sind ungünstiger):", - " - unter 420 min: below the recommended adult range", + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 423.85 min is in the \"the recommended adult range\" band, and sits near the LOWER boundary of this band (nearest boundary at 420 min).", + "- Guideline bands (NSF 2015; AASM/SRS 2015; the middle band is the target — both extremes are less favourable):", + " - below 420 min: below the recommended adult range", " - 420–540 min: the recommended adult range", - " - ab 540 min: above the recommended adult range", - "- Werte im Band \"the recommended adult range\" gelten laut diesen Leitlinien als ein gesunder / risikoarmer Bereich.", - "- Benenne, wo der Wert liegt und was dieses Band praktisch bedeutet. Beurteile jeden Trend GEMÄSS dieser Position: eine Verschiebung tief in einem günstigen Band ist eine Randnotiz; dieselbe Verschiebung nahe an oder über einer Grenze ist die Schlagzeile. Rahme die Konsequenz ohne Diagnose (\"Werte in diesem Bereich gelten als …\", nie \"du hast / bist gefährdet für …\").", + " - 540 min and above: above the recommended adult range", + "- Values in the \"the recommended adult range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", "", "", - "{\"locale\":\"de\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"SLEEP_DURATION\",\"metric\":{\"id\":\"SLEEP_DURATION\",\"displayName\":\"Sleep duration\",\"unit\":\"min\",\"direction\":\"target-band\",\"normalRange\":{\"low\":420,\"high\":540}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":89,\"totalSpanDays\":88,\"newestMeasurementDaysAgo\":1},\"SLEEP_DURATION\":{\"signal\":{\"metric\":\"Sleep duration\",\"unit\":\"min\",\"current\":423.85,\"currentWindowDays\":20,\"baseline\":404.11,\"baselineLabel\":\"your recent-weeks average\",\"delta\":19.74,\"deltaPct\":4.9,\"spread\":25.3,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":420,\"high\":540},\"placement\":\"in band\"},\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":375,\"max\":380,\"mean\":376.6,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":379,\"max\":414,\"mean\":394.86,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":421,\"max\":445,\"mean\":435.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":418,\"max\":445,\"mean\":434,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":378,\"max\":411,\"mean\":392.57,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":375,\"max\":391,\"mean\":380.43,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":397,\"max\":436,\"mean\":417.14,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":438,\"max\":445,\"mean\":442.29,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":394,\"max\":433,\"mean\":413.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":375,\"max\":388,\"mean\":379,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":379,\"max\":379,\"mean\":379,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":417}}}" - ] + "{\"locale\":\"fr\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"SLEEP_DURATION\",\"metric\":{\"id\":\"SLEEP_DURATION\",\"displayName\":\"Sleep duration\",\"unit\":\"min\",\"direction\":\"target-band\",\"normalRange\":{\"low\":420,\"high\":540}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":89,\"totalSpanDays\":88,\"newestMeasurementDaysAgo\":1},\"SLEEP_DURATION\":{\"signal\":{\"metric\":\"Sleep duration\",\"unit\":\"min\",\"current\":423.85,\"currentWindowDays\":20,\"baseline\":404.11,\"baselineLabel\":\"your recent-weeks average\",\"delta\":19.74,\"deltaPct\":4.9,\"spread\":25.3,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":420,\"high\":540},\"placement\":\"in band\"},\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":375,\"max\":380,\"mean\":376.6,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":379,\"max\":414,\"mean\":394.86,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":421,\"max\":445,\"mean\":435.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":418,\"max\":445,\"mean\":434,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":378,\"max\":411,\"mean\":392.57,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":375,\"max\":391,\"mean\":380.43,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":397,\"max\":436,\"mean\":417.14,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":438,\"max\":445,\"mean\":442.29,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":394,\"max\":433,\"mean\":413.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":375,\"max\":388,\"mean\":379,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":379,\"max\":379,\"mean\":379,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":417}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Das ist kein JSON.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "SLEEP_DURATION unparseable-fr": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -4638,1512 +5820,330 @@ "", "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with what this means for the user today, then the number behind it.", - "Write one short assessment of this person's sleep duration. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 89 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", - "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 3-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 4 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", - "", - "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Quand la durée de sommeil monte, le poids du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=45, r=0.98)", - "- Quand la glycémie monte, la durée de sommeil du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=22, r=0.95)", - "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", - "", - "", - "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", - "- Current value 423.85 min is in the \"the recommended adult range\" band, and sits near the LOWER boundary of this band (nearest boundary at 420 min).", - "- Guideline bands (NSF 2015; AASM/SRS 2015; the middle band is the target — both extremes are less favourable):", - " - below 420 min: below the recommended adult range", - " - 420–540 min: the recommended adult range", - " - 540 min and above: above the recommended adult range", - "- Values in the \"the recommended adult range\" band are considered a healthy / low-risk range per these guidelines.", - "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", - "", - "", - "{\"locale\":\"fr\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"SLEEP_DURATION\",\"metric\":{\"id\":\"SLEEP_DURATION\",\"displayName\":\"Sleep duration\",\"unit\":\"min\",\"direction\":\"target-band\",\"normalRange\":{\"low\":420,\"high\":540}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":89,\"totalSpanDays\":88,\"newestMeasurementDaysAgo\":1},\"SLEEP_DURATION\":{\"signal\":{\"metric\":\"Sleep duration\",\"unit\":\"min\",\"current\":423.85,\"currentWindowDays\":20,\"baseline\":404.11,\"baselineLabel\":\"your recent-weeks average\",\"delta\":19.74,\"deltaPct\":4.9,\"spread\":25.3,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":420,\"high\":540},\"placement\":\"in band\"},\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":375,\"max\":380,\"mean\":376.6,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":379,\"max\":414,\"mean\":394.86,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":421,\"max\":445,\"mean\":435.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":418,\"max\":445,\"mean\":434,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":378,\"max\":411,\"mean\":392.57,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":375,\"max\":391,\"mean\":380.43,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":397,\"max\":436,\"mean\":417.14,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":438,\"max\":445,\"mean\":442.29,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":394,\"max\":433,\"mean\":413.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":375,\"max\":388,\"mean\":379,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":379,\"max\":379,\"mean\":379,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":417}}}" - ] - } - ], "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], "result": { + "cached": true, "hasProvider": false, "text": "Stable, comme d’habitude. Sleep duration est à 423,9 min en ce moment — au-dessus de ta moyenne habituelle de 404,1 min. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", - "cached": true, "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "SLEEP_DURATION blocked-es": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in Spanish: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "ARCHETYPE — SLEEP:", - "- This is nightly sleep duration. The snapshot's values are in minutes; convert to hours for your prose (e.g. 450 min = 7.5 h). Assess both the typical duration and its CONSISTENCY: a stable ~7–9 h is the favourable pattern; high night-to-night variability is itself worth naming even when the mean looks fine.", - "- Compare the recent nights to the user's own weekly/monthly baseline. A reference band of 7–9 h is a coarse adult guide, not a verdict — short or long sleepers vary.", - "- Close with one doable step toward more consistent or sufficient sleep ONLY when the finding implies one — never the forbidden 'get enough sleep' platitude; be specific (e.g. a steadier wind-down time). When sleep is already consistent and sufficient, affirm it and name one thing worth keeping an eye on instead of manufacturing a step.", - "", - "METRIC: Sleep duration (unit: min).", - "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", - "Coarse population reference band: 420–540 min (a placement aid only — the user's OWN baseline leads).", - "", - "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with what this means for the user today, then the number behind it.", - "Write one short assessment of this person's sleep duration. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 89 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", - "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 3-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 4 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", - "", - "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Cuando la duración del sueño es mayor, el peso del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=45, r=0.98)", - "- Cuando la glucosa en sangre es mayor, la duración del sueño del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=22, r=0.95)", - "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", - "", - "", - "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", - "- Current value 423.85 min is in the \"the recommended adult range\" band, and sits near the LOWER boundary of this band (nearest boundary at 420 min).", - "- Guideline bands (NSF 2015; AASM/SRS 2015; the middle band is the target — both extremes are less favourable):", - " - below 420 min: below the recommended adult range", - " - 420–540 min: the recommended adult range", - " - 540 min and above: above the recommended adult range", - "- Values in the \"the recommended adult range\" band are considered a healthy / low-risk range per these guidelines.", - "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", - "", - "", - "{\"locale\":\"es\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"SLEEP_DURATION\",\"metric\":{\"id\":\"SLEEP_DURATION\",\"displayName\":\"Sleep duration\",\"unit\":\"min\",\"direction\":\"target-band\",\"normalRange\":{\"low\":420,\"high\":540}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":89,\"totalSpanDays\":88,\"newestMeasurementDaysAgo\":1},\"SLEEP_DURATION\":{\"signal\":{\"metric\":\"Sleep duration\",\"unit\":\"min\",\"current\":423.85,\"currentWindowDays\":20,\"baseline\":404.11,\"baselineLabel\":\"your recent-weeks average\",\"delta\":19.74,\"deltaPct\":4.9,\"spread\":25.3,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":420,\"high\":540},\"placement\":\"in band\"},\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":375,\"max\":380,\"mean\":376.6,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":379,\"max\":414,\"mean\":394.86,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":421,\"max\":445,\"mean\":435.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":418,\"max\":445,\"mean\":434,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":378,\"max\":411,\"mean\":392.57,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":375,\"max\":391,\"mean\":380.43,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":397,\"max\":436,\"mean\":417.14,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":438,\"max\":445,\"mean\":442.29,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":394,\"max\":433,\"mean\":413.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":375,\"max\":388,\"mean\":379,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":379,\"max\":379,\"mean\":379,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":417}}}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 700, - "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Estable, como de costumbre. Sleep duration está en 423,9 min ahora mismo — por encima de tu promedio habitual de 404,1 min. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "SLEEP_DURATION bare-error-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "insufficient": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "SLEEP_DURATION bare-error-de": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "insufficient": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "SLEEP_DURATION keyless-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Steady and much as usual. Sleep duration is at 423.9 min right now — higher than your usual average of 404.1 min. That sits in your usual range — nothing you need to act on right now.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "SLEEP_DURATION keyless-de": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Stabil und wie gewohnt. Sleep duration liegt aktuell bei 423,9 min — höher als dein üblicher Schnitt von 404,1 min. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "SLEEP_DURATION keyless-read-only": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] + } }, "VO2_MAX no-data": { "calls": [], "enqueued": [], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "insufficient": true - }, "notes": [ { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": null, + "retryAt": null, "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:BLOOD_GLUCOSE", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", "metric": "metric:BLOOD_GLUCOSE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": null, "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:BLOOD_GLUCOSE", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, + "retryAt": null, "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:RESTING_HEART_RATE", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "de", "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "metric:SLEEP_DURATION", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:RESTING_HEART_RATE", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "metric:SLEEP_DURATION", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "metric:SLEEP_DURATION", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": false, + "hasProvider": true, + "insufficient": true, + "text": null, + "updatedAt": null + } } } diff --git a/tests/integration/__golden__/status-parity/mood.json b/tests/integration/__golden__/status-parity/mood.json index 0d7f88835..cc25c6355 100644 --- a/tests/integration/__golden__/status-parity/mood.json +++ b/tests/integration/__golden__/status-parity/mood.json @@ -1,388 +1,30 @@ { - "read-only-cold": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "mood", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "preparing": true, - "revalidating": false - }, - "notes": [] - }, - "fresh-en": { + "bare-error-de": { "calls": [ { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — MOOD / WELL-BEING:", - "- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).", - "- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", - "- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.", - "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", - "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", - "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", - "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." - ], + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with what this means for the user today, then the number behind it.", - "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zur Stimmung dieser Person. Beginne mit der BEDEUTUNG des jüngsten Musters in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"eine hellere Phase als sonst\", \"stabil, hält sich, wo sie war\") — und bring danach das jüngste Niveau als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem freundlichen, machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Eintragsanzahl und Aktualität ableiten.", "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", "", "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", "", - "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", + "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", "", "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"your mood\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" - ] + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":0,\"totalSpanDays\":0,\"newestEntryDaysAgo\":null},\"mood\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null,\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":null},\"tags\":null,\"tagInfluence\":null,\"betterDays\":null,\"factorCrosstab\":null,\"narratives\":null,\"timeOfDay\":null,\"stability\":null},\"crossMetricContext\":null}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "cached-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "forced-unchanged-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "next-day-en": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — MOOD / WELL-BEING:", - "- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).", - "- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", - "- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.", - "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", - "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", - "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", - "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-16", - "OPENER HINT: Open with the direction of change over time, then say where it stands now.", - "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", - "", - "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", - "Parity note for fresh-en.", - "", - "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 70 day-buckets in the window, latest reading 1 day ago.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":1},\"mood\":{\"signal\":{\"metric\":\"your mood\",\"unit\":\"/5\",\"current\":3.05,\"currentWindowDays\":21,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.19,\"deltaPct\":-5.9,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":21,\"newestDaysAgo\":1,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":41.4},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"41\"},\"strength\":0.2}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 250, "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for next-day-en.", - "cached": false, - "updatedAt": "2026-06-16T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "read-only-stale": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "mood", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": "Parity note for next-day-en.", - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z", - "preparing": false, - "revalidating": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "prose-de": { - "calls": [ - { "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -468,72 +110,124 @@ "- Cross-Metrik: crossMetricContext trägt Korrelationen zu Gewicht, Blutdruck und Puls; nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache. Erzwinge keinen Querverweis ohne klares Muster.", "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren, freundlichen Schritt, wenn der Befund einen nahelegt (z.B. an guten Tagen kurz festhalten, was geholfen hat). Bei stabil guter Stimmung das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", - "Schreibe eine kurze Einschätzung zur Stimmung dieser Person. Beginne mit der BEDEUTUNG des jüngsten Musters in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"eine hellere Phase als sonst\", \"stabil, hält sich, wo sie war\") — und bring danach das jüngste Niveau als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem freundlichen, machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Eintragsanzahl und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"deine Stimmung\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": true, - "text": "Das ist kein JSON.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, "notes": [ { - "userId": "parity-rich", - "metric": "mood", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "mood", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" }, { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": null, "inputHash": null, - "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "mood", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "mood", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Für diese Karte liegt gerade keine Einschätzung vor. Stimmung liest sich über Wochen, nicht über einzelne Tage — wiederkehrende Muster und wie sie zu den anderen Werten passen, sind das, was trägt. Eine anhaltende Phase niedriger Stimmung ist die, die Aufmerksamkeit verdient.", + "updatedAt": null + } }, - "unparseable-fr": { + "bare-error-en": { "calls": [ { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the single most recent value and where it sits — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":0,\"totalSpanDays\":0,\"newestEntryDaysAgo\":null},\"mood\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null,\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":null},\"tags\":null,\"tagInfluence\":null,\"betterDays\":null,\"factorCrosstab\":null,\"narratives\":null,\"timeOfDay\":null,\"stability\":null},\"crossMetricContext\":null}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -607,7 +301,7 @@ "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", "{ \"summary\": \"...\" }", "", "METRIC — MOOD / WELL-BEING:", @@ -617,13 +311,93 @@ "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", - "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step.", - "", - "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "mood", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "mood", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. Mood reads over weeks rather than single days — recurring patterns, and how they line up with your other metrics, are what carry. A sustained low stretch is the one that deserves attention.", + "updatedAt": null + } + }, + "blocked-es": { + "calls": [ + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open with what this means for the user today, then the number behind it.", @@ -637,69 +411,13 @@ "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", "", "", - "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"deine Stimmung\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" - ] + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"deine Stimmung\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Stable, comme d’habitude. Ton humeur est à 3,2 en ce moment — en dessous de ta moyenne habituelle de 3,2. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "mood", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "blocked-es": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -787,9 +505,128 @@ "", "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "mood", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "mood", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Estable, como de costumbre. Tu estado de ánimo está en 3,2 ahora mismo — por debajo de tu promedio habitual de 3,2. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", + "updatedAt": null + } + }, + "cached-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "fresh-en": { + "calls": [ + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open with what this means for the user today, then the number behind it.", @@ -798,87 +635,18 @@ "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", "", "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", "", - "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", "", "", - "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"deine Stimmung\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" - ] + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"your mood\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Estable, como de costumbre. Tu estado de ánimo está en 3,2 ahora mismo — por debajo de tu promedio habitual de 3,2. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "mood", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "bare-error-en": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -964,111 +732,470 @@ "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with what this means for the user today, then the number behind it.", - "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the single most recent value and where it sits — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":0,\"totalSpanDays\":0,\"newestEntryDaysAgo\":null},\"mood\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null,\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":null},\"tags\":null,\"tagInfluence\":null,\"betterDays\":null,\"factorCrosstab\":null,\"narratives\":null,\"timeOfDay\":null,\"stability\":null},\"crossMetricContext\":null}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. Mood reads over weeks rather than single days — recurring patterns, and how they line up with your other metrics, are what carry. A sustained low stretch is the one that deserves attention.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "mood", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "mood", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" }, { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": null, "inputHash": null, - "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "mood", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "mood", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "mood", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "mood", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Stabil und wie gewohnt. Deine Stimmung liegt aktuell bei 3,2 — niedriger als dein üblicher Schnitt von 3,2. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", + "updatedAt": null + } }, - "bare-error-de": { + "keyless-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "mood", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "mood", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "mood", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Steady and much as usual. Your mood is at 3.2 right now — lower than your usual average of 3.2. That sits in your usual range — nothing you need to act on right now.", + "updatedAt": null + } + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "mood", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "mood", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "mood", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. Mood reads over weeks rather than single days — recurring patterns, and how they line up with your other metrics, are what carry. A sustained low stretch is the one that deserves attention.", + "updatedAt": null + } + }, + "next-day-en": { "calls": [ { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 70 day-buckets in the window, latest reading 1 day ago.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":1},\"mood\":{\"signal\":{\"metric\":\"your mood\",\"unit\":\"/5\",\"current\":3.05,\"currentWindowDays\":21,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.19,\"deltaPct\":-5.9,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":21,\"newestDaysAgo\":1,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":41.4},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"41\"},\"strength\":0.2}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MOOD / WELL-BEING:", + "- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).", + "- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.", + "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", + "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", + "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", + "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "prose-de": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zur Stimmung dieser Person. Beginne mit der BEDEUTUNG des jüngsten Musters in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"eine hellere Phase als sonst\", \"stabil, hält sich, wo sie war\") — und bring danach das jüngste Niveau als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem freundlichen, machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Eintragsanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"deine Stimmung\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -1154,389 +1281,262 @@ "- Cross-Metrik: crossMetricContext trägt Korrelationen zu Gewicht, Blutdruck und Puls; nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache. Erzwinge keinen Querverweis ohne klares Muster.", "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren, freundlichen Schritt, wenn der Befund einen nahelegt (z.B. an guten Tagen kurz festhalten, was geholfen hat). Bei stabil guter Stimmung das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", - "Schreibe eine kurze Einschätzung zur Stimmung dieser Person. Beginne mit der BEDEUTUNG des jüngsten Musters in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"eine hellere Phase als sonst\", \"stabil, hält sich, wo sie war\") — und bring danach das jüngste Niveau als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem freundlichen, machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Eintragsanzahl und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", - "", - "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":0,\"totalSpanDays\":0,\"newestEntryDaysAgo\":null},\"mood\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null,\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":null},\"tags\":null,\"tagInfluence\":null,\"betterDays\":null,\"factorCrosstab\":null,\"narratives\":null,\"timeOfDay\":null,\"stability\":null},\"crossMetricContext\":null}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "Für diese Karte liegt gerade keine Einschätzung vor. Stimmung liest sich über Wochen, nicht über einzelne Tage — wiederkehrende Muster und wie sie zu den anderen Werten passen, sind das, was trägt. Eine anhaltende Phase niedriger Stimmung ist die, die Aufmerksamkeit verdient.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "mood", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "mood", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "mood", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" } - ] + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Das ist kein JSON.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } }, - "keyless-en": { + "read-only-cold": { "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Steady and much as usual. Your mood is at 3.2 right now — lower than your usual average of 3.2. That sits in your usual range — nothing you need to act on right now.", - "cached": true, - "updatedAt": null - }, - "notes": [ + "enqueued": [ { - "userId": "parity-bare", - "metric": "mood", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "mood", "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", "metric": "mood", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "userId": "parity-rich" } - ] + ], + "notes": [], + "result": { + "cached": false, + "hasProvider": true, + "preparing": true, + "revalidating": false, + "text": null, + "updatedAt": null + } }, - "keyless-de": { + "read-only-stale": { "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Stabil und wie gewohnt. Deine Stimmung liegt aktuell bei 3,2 — niedriger als dein üblicher Schnitt von 3,2. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", - "cached": true, - "updatedAt": null - }, + "enqueued": [ + { + "locale": "en", + "metric": "mood", + "userId": "parity-rich" + } + ], "notes": [ { - "userId": "parity-bare", - "metric": "mood", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "mood", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": true, + "preparing": false, + "revalidating": true, + "text": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } }, - "keyless-read-only": { - "calls": [], + "unparseable-fr": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"deine Stimmung\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MOOD / WELL-BEING:", + "- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).", + "- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.", + "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", + "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", + "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", + "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. Mood reads over weeks rather than single days — recurring patterns, and how they line up with your other metrics, are what carry. A sustained low stretch is the one that deserves attention.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "mood", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "mood", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "mood", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "mood", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "mood", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "mood", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "mood", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "mood", + "inputHash": null, + "items": null, "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "mood", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Stable, comme d’habitude. Ton humeur est à 3,2 en ce moment — en dessous de ta moyenne habituelle de 3,2. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", + "updatedAt": null + } } } diff --git a/tests/integration/__golden__/status-parity/pulse.json b/tests/integration/__golden__/status-parity/pulse.json index d1fb79925..a7a371fbf 100644 --- a/tests/integration/__golden__/status-parity/pulse.json +++ b/tests/integration/__golden__/status-parity/pulse.json @@ -1,386 +1,30 @@ { - "read-only-cold": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "preparing": true, - "revalidating": false - }, - "notes": [] - }, - "fresh-en": { + "bare-error-de": { "calls": [ { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — PULSE / HEART RATE:", - "- The snapshot carries pulse.signal (the finished comparison), pulse.summary + pulse.series (graded) and pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus shows the latest daily value and the step from the prior day.", - "- Trend over weeks: lead from pulse.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", - "- Direction: a resting pulse falling over weeks suggests improving cardiovascular fitness; a rising one can point to stress, sleep loss or overtraining — as a possible explanation, never a diagnosis.", - "- Placement anchors (resting pulse): athletic 50-60, normal 60-100. Values in the 80-100 band are technically normal but tend to be more favourable lower over the long run. < 50 or > 100 across several days is worth noting without alarm.", - "- Medication link: beta-blockers lower heart rate. Mention moodContext.moodVsPulseCorrelation and any pulse-vs-systolic association only when present and |r| > 0.4 — as an association, never a cause.", - "- One message: if the resting pulse sits above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving or steady, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." - ], + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", - "Write one short assessment of this person's resting pulse. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady, right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zum Ruhepuls dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil, genau da, wo er war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", "", "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", "", - "DATA STRENGTH: 122 day-buckets in the window, latest reading today.", + "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", "", "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"your resting pulse\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" - ] + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"pulse\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null,\"target\":{\"greenMin\":60,\"greenMax\":100,\"orangeMin\":55,\"orangeMax\":105,\"inTargetPctLast30DailyPoints\":null}},\"moodContext\":null}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "cached-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "forced-unchanged-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": null, - "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "next-day-en": { - "calls": [ - { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — PULSE / HEART RATE:", - "- The snapshot carries pulse.signal (the finished comparison), pulse.summary + pulse.series (graded) and pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus shows the latest daily value and the step from the prior day.", - "- Trend over weeks: lead from pulse.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", - "- Direction: a resting pulse falling over weeks suggests improving cardiovascular fitness; a rising one can point to stress, sleep loss or overtraining — as a possible explanation, never a diagnosis.", - "- Placement anchors (resting pulse): athletic 50-60, normal 60-100. Values in the 80-100 band are technically normal but tend to be more favourable lower over the long run. < 50 or > 100 across several days is worth noting without alarm.", - "- Medication link: beta-blockers lower heart rate. Mention moodContext.moodVsPulseCorrelation and any pulse-vs-systolic association only when present and |r| > 0.4 — as an association, never a cause.", - "- One message: if the resting pulse sits above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving or steady, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." - ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-16", - "OPENER HINT: Open with the overall read in plain words, then bring in the number as support — not number-first.", - "Write one short assessment of this person's resting pulse. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady, right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", - "Parity note for fresh-en.", - "", - "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 122 day-buckets in the window, latest reading 1 day ago.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":1},\"pulse\":{\"signal\":{\"metric\":\"your resting pulse\",\"unit\":\"bpm\",\"current\":72.1,\"currentWindowDays\":21,\"baseline\":71.76,\"baselineLabel\":\"your monthly average\",\"delta\":0.34,\"deltaPct\":0.5,\"spread\":2.15,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":62,\"newestDaysAgo\":1,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":74,\"mean\":65.67,\"n\":17},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":74,\"mean\":65,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":62,\"max\":83,\"mean\":72.29,\"n\":49}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":41.4}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" - ] - } - ], - "temperature": 0.45, - "maxTokens": 250, "seed": 1618033, - "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for next-day-en.", - "cached": false, - "updatedAt": "2026-06-16T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "read-only-stale": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": "Parity note for next-day-en.", - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z", - "preparing": false, - "revalidating": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "prose-de": { - "calls": [ - { "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -465,72 +109,124 @@ "- Medikamenten-Bezug: Betablocker senken die Herzfrequenz. moodContext.moodVsPulseCorrelation und ein pulseVsSystolic-Zusammenhang nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", "- Eine Botschaft: Liegt der Ruhepuls über der eigenen Baseline, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage morgens vor dem Aufstehen messen, um eine saubere Ruhe-Baseline zu bekommen). Verbessert er sich oder ist er stabil, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen." ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", - "Schreibe eine kurze Einschätzung zum Ruhepuls dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil, genau da, wo er war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 122 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"dein Ruhepuls\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": true, - "text": "Das ist kein JSON.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, "notes": [ { - "userId": "parity-rich", - "metric": "pulse", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "pulse", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" }, { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": null, "inputHash": null, - "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "pulse", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "pulse", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Für diese Karte liegt gerade keine Einschätzung vor. Der Ruhepuls liest sich am besten entspannt und zur gleichen Tageszeit gemessen. Kurze Ausschläge sind gewöhnlich; was auffällt, ist ein Muster, das über mehrere Tage hält, oder wiederholtes Abdriften vom eigenen üblichen Bereich.", + "updatedAt": null + } }, - "unparseable-fr": { + "bare-error-en": { "calls": [ { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's resting pulse. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady, right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"pulse\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null,\"target\":{\"greenMin\":60,\"greenMax\":100,\"orangeMin\":55,\"orangeMax\":105,\"inTargetPctLast30DailyPoints\":null}},\"moodContext\":null}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -604,7 +300,7 @@ "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", "{ \"summary\": \"...\" }", "", "METRIC — PULSE / HEART RATE:", @@ -613,13 +309,93 @@ "- Direction: a resting pulse falling over weeks suggests improving cardiovascular fitness; a rising one can point to stress, sleep loss or overtraining — as a possible explanation, never a diagnosis.", "- Placement anchors (resting pulse): athletic 50-60, normal 60-100. Values in the 80-100 band are technically normal but tend to be more favourable lower over the long run. < 50 or > 100 across several days is worth noting without alarm.", "- Medication link: beta-blockers lower heart rate. Mention moodContext.moodVsPulseCorrelation and any pulse-vs-systolic association only when present and |r| > 0.4 — as an association, never a cause.", - "- One message: if the resting pulse sits above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving or steady, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step.", - "", - "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + "- One message: if the resting pulse sits above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving or steady, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "pulse", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "pulse", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. Resting pulse reads best taken relaxed and at the same time of day. Short spikes are ordinary; what stands out is a pattern holding across several days, or repeated drift away from your usual range.", + "updatedAt": null + } + }, + "blocked-es": { + "calls": [ + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", @@ -633,69 +409,13 @@ "DATENLAGE: 122 Tages-Buckets im Fenster, letzter Wert heute.", "", "", - "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"dein Ruhepuls\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" - ] + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"dein Ruhepuls\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Stable, comme d’habitude. Ton pouls au repos est à 72 bpm en ce moment — en dessous de ta moyenne habituelle de 72 bpm. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "blocked-es": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -782,9 +502,128 @@ "", "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "pulse", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "pulse", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Estable, como de costumbre. Tu pulso en reposo está en 72 bpm ahora mismo — por debajo de tu promedio habitual de 72 bpm. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", + "updatedAt": null + } + }, + "cached-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "fresh-en": { + "calls": [ + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", @@ -793,87 +632,18 @@ "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", "", "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", "", - "DATENLAGE: 122 Tages-Buckets im Fenster, letzter Wert heute.", + "DATA STRENGTH: 122 day-buckets in the window, latest reading today.", "", "", - "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"dein Ruhepuls\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" - ] + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"your resting pulse\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Estable, como de costumbre. Tu pulso en reposo está en 72 bpm ahora mismo — por debajo de tu promedio habitual de 72 bpm. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "bare-error-en": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -958,111 +728,469 @@ "- Medication link: beta-blockers lower heart rate. Mention moodContext.moodVsPulseCorrelation and any pulse-vs-systolic association only when present and |r| > 0.4 — as an association, never a cause.", "- One message: if the resting pulse sits above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving or steady, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", - "Write one short assessment of this person's resting pulse. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady, right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"pulse\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null,\"target\":{\"greenMin\":60,\"greenMax\":100,\"orangeMin\":55,\"orangeMax\":105,\"inTargetPctLast30DailyPoints\":null}},\"moodContext\":null}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. Resting pulse reads best taken relaxed and at the same time of day. Short spikes are ordinary; what stands out is a pattern holding across several days, or repeated drift away from your usual range.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "pulse", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "pulse", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" }, { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, + "dateKey": "2026-06-15", + "generatedAt": null, "inputHash": null, - "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "pulse", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "pulse", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "pulse", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "pulse", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Stabil und wie gewohnt. Dein Ruhepuls liegt aktuell bei 72 bpm — niedriger als dein üblicher Schnitt von 72 bpm. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", + "updatedAt": null + } }, - "bare-error-de": { + "keyless-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "pulse", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "pulse", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "pulse", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Steady and much as usual. Your resting pulse is at 72 bpm right now — lower than your usual average of 72 bpm. That sits in your usual range — nothing you need to act on right now.", + "updatedAt": null + } + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "pulse", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "pulse", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "pulse", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. Resting pulse reads best taken relaxed and at the same time of day. Short spikes are ordinary; what stands out is a pattern holding across several days, or repeated drift away from your usual range.", + "updatedAt": null + } + }, + "next-day-en": { "calls": [ { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open with the overall read in plain words, then bring in the number as support — not number-first.", + "Write one short assessment of this person's resting pulse. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady, right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 122 day-buckets in the window, latest reading 1 day ago.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":1},\"pulse\":{\"signal\":{\"metric\":\"your resting pulse\",\"unit\":\"bpm\",\"current\":72.1,\"currentWindowDays\":21,\"baseline\":71.76,\"baselineLabel\":\"your monthly average\",\"delta\":0.34,\"deltaPct\":0.5,\"spread\":2.15,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":62,\"newestDaysAgo\":1,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":74,\"mean\":65.67,\"n\":17},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":74,\"mean\":65,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":62,\"max\":83,\"mean\":72.29,\"n\":49}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":41.4}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — PULSE / HEART RATE:", + "- The snapshot carries pulse.signal (the finished comparison), pulse.summary + pulse.series (graded) and pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus shows the latest daily value and the step from the prior day.", + "- Trend over weeks: lead from pulse.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Direction: a resting pulse falling over weeks suggests improving cardiovascular fitness; a rising one can point to stress, sleep loss or overtraining — as a possible explanation, never a diagnosis.", + "- Placement anchors (resting pulse): athletic 50-60, normal 60-100. Values in the 80-100 band are technically normal but tend to be more favourable lower over the long run. < 50 or > 100 across several days is worth noting without alarm.", + "- Medication link: beta-blockers lower heart rate. Mention moodContext.moodVsPulseCorrelation and any pulse-vs-systolic association only when present and |r| > 0.4 — as an association, never a cause.", + "- One message: if the resting pulse sits above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving or steady, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "prose-de": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zum Ruhepuls dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil, genau da, wo er war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 122 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"dein Ruhepuls\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -1147,389 +1275,261 @@ "- Medikamenten-Bezug: Betablocker senken die Herzfrequenz. moodContext.moodVsPulseCorrelation und ein pulseVsSystolic-Zusammenhang nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", "- Eine Botschaft: Liegt der Ruhepuls über der eigenen Baseline, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage morgens vor dem Aufstehen messen, um eine saubere Ruhe-Baseline zu bekommen). Verbessert er sich oder ist er stabil, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen." ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", - "Schreibe eine kurze Einschätzung zum Ruhepuls dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil, genau da, wo er war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", - "", - "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"pulse\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null,\"target\":{\"greenMin\":60,\"greenMax\":100,\"orangeMin\":55,\"orangeMax\":105,\"inTargetPctLast30DailyPoints\":null}},\"moodContext\":null}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "Für diese Karte liegt gerade keine Einschätzung vor. Der Ruhepuls liest sich am besten entspannt und zur gleichen Tageszeit gemessen. Kurze Ausschläge sind gewöhnlich; was auffällt, ist ein Muster, das über mehrere Tage hält, oder wiederholtes Abdriften vom eigenen üblichen Bereich.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "pulse", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "pulse", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "pulse", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" } - ] + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Das ist kein JSON.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } }, - "keyless-en": { + "read-only-cold": { "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Steady and much as usual. Your resting pulse is at 72 bpm right now — lower than your usual average of 72 bpm. That sits in your usual range — nothing you need to act on right now.", - "cached": true, - "updatedAt": null - }, - "notes": [ + "enqueued": [ { - "userId": "parity-bare", - "metric": "pulse", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "pulse", "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", "metric": "pulse", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "userId": "parity-rich" } - ] + ], + "notes": [], + "result": { + "cached": false, + "hasProvider": true, + "preparing": true, + "revalidating": false, + "text": null, + "updatedAt": null + } }, - "keyless-de": { + "read-only-stale": { "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Stabil und wie gewohnt. Dein Ruhepuls liegt aktuell bei 72 bpm — niedriger als dein üblicher Schnitt von 72 bpm. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", - "cached": true, - "updatedAt": null - }, + "enqueued": [ + { + "locale": "en", + "metric": "pulse", + "userId": "parity-rich" + } + ], "notes": [ { - "userId": "parity-bare", - "metric": "pulse", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "pulse", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": true, + "preparing": false, + "revalidating": true, + "text": "Parity note for next-day-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } }, - "keyless-read-only": { - "calls": [], + "unparseable-fr": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's resting pulse. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady, right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 122 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"dein Ruhepuls\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — PULSE / HEART RATE:", + "- The snapshot carries pulse.signal (the finished comparison), pulse.summary + pulse.series (graded) and pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus shows the latest daily value and the step from the prior day.", + "- Trend over weeks: lead from pulse.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Direction: a resting pulse falling over weeks suggests improving cardiovascular fitness; a rising one can point to stress, sleep loss or overtraining — as a possible explanation, never a diagnosis.", + "- Placement anchors (resting pulse): athletic 50-60, normal 60-100. Values in the 80-100 band are technically normal but tend to be more favourable lower over the long run. < 50 or > 100 across several days is worth noting without alarm.", + "- Medication link: beta-blockers lower heart rate. Mention moodContext.moodVsPulseCorrelation and any pulse-vs-systolic association only when present and |r| > 0.4 — as an association, never a cause.", + "- One message: if the resting pulse sits above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving or steady, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. Resting pulse reads best taken relaxed and at the same time of day. Short spikes are ordinary; what stands out is a pattern holding across several days, or repeated drift away from your usual range.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "pulse", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "pulse", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "pulse", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": null, - "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "de", + "metric": "pulse", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "pulse", - "locale": "en", - "text": "Parity note for next-day-en.", - "items": null, - "inputHash": null, - "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": null, + "items": null, + "locale": "en", + "metric": "pulse", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "text": "Parity note for next-day-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "pulse", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "pulse", + "inputHash": null, + "items": null, "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "pulse", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Stable, comme d’habitude. Ton pouls au repos est à 72 bpm en ce moment — en dessous de ta moyenne habituelle de 72 bpm. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", + "updatedAt": null + } } } diff --git a/tests/integration/__golden__/status-parity/weight.json b/tests/integration/__golden__/status-parity/weight.json index 3ae667d67..5ad00c1be 100644 --- a/tests/integration/__golden__/status-parity/weight.json +++ b/tests/integration/__golden__/status-parity/weight.json @@ -1,282 +1,30 @@ { - "read-only-cold": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "weight", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": null, - "cached": false, - "updatedAt": null, - "preparing": true, - "revalidating": false - }, - "notes": [] - }, - "fresh-en": { + "bare-error-de": { "calls": [ { - "system": [ - "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", - "", - "YOUR DATA (graded snapshot):", - "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", - "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", - "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", - "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", - "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", - "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", - "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", - "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", - "", - "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", - "Moves available (choose the few that genuinely apply):", - "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", - "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", - "(c) name a genuine, earned win or a steady streak when the data shows one;", - "(d) one watch-item worth keeping an eye on;", - "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", - "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", - "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", - "", - "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", - "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", - "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", - "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", - "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", - "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", - "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", - "", - "OPENING — lead with meaning, not the metric value", - "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", - "", - "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", - "", - "JUDGE THE DATA HONESTLY (never invent a trend):", - "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", - "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", - "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", - "", - "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", - "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", - "", - "METRIC IDENTIFIERS STAY OUT OF PROSE", - "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", - "", - "GLP-1 DOSE SAFETY (contract, not style)", - "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", - "", - "ACUTE RED FLAGS (contract, not style)", - "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", - "", - "GROUNDING — ZERO HALLUCINATIONS", - "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", - "", - "TONE — a warm, motivating advisor, never clinical or alarming", - "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", - "", - "NOT AN INSIGHT — never recite mechanics", - "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", - "", - "INTERPRET, DON'T ENUMERATE", - "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", - "", - "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", - "", - "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", - "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", - "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", - "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", - "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", - "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", - "{ \"summary\": \"...\" }", - "", - "METRIC — WEIGHT:", - "- The snapshot carries weight.signal (the finished comparison) + weight.summary + weight.series (graded). weight.latestDayFocus shows the latest daily value, the step from the previous measured day and, where present, the same-day blood pressure.", - "- This card's focus is the continuous TREND and PACE over time (pace per week, plateaus, milestones), not WHO-band placement — the BMI card carries that. Mention the BMI value only as a side reference, never make the band the central message here.", - "- The weekly/monthly trend matters, not the single value: lead from weight.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Day-to-day swings of 2.2 lb-4.4 lb (water, time of day) are normal and not a finding.", - "- Own target: when the snapshot carries weight.target, that is the target band the person set themselves, and weight.target.progress says which way is progress (gaining, losing, or holding steady). Judge every change AGAINST that target: below it, a gain is progress and never a setback or a regression. The loss pace and milestone guidance below applies only when losing is the progress direction or no target is set.", - "- Plateau: if the mean moves < 1.1 lb over > ~2 weeks, that is a plateau — name it honestly for someone who is losing, without discouraging.", - "- Pace: sustainable loss is ~1.1 lb-2.2 lb per week (DGE). Sustained > 2.2 lb per week is more likely water/muscle than fat — frame cautiously.", - "- Milestones against their own maximum: ~5% loss from the highest value (series max) already brings metabolic benefit, ~10% a substantial one — acknowledge explicitly when reached.", - "- BMI reference only as a brief aside when height is in the profile (do not claim it otherwise); the WHO-band placement itself belongs on the BMI card, not here.", - "- Associations: mention weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation and moodContext.moodVsWeightCorrelation only when present and |r| > 0.4 — as an association, never a cause.", - "- One message: close with ONE doable step that fits the direction ONLY when the finding implies one (e.g. on a plateau, weigh at the same time of day to see the real trend rather than the daily noise). When the trend is steady and there is nothing useful to do, affirm it honestly and name one thing worth keeping an eye on instead of manufacturing a step." - ], + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", - "Write one short assessment of this person's weight. Open with what the trend MEANS in plain words — the direction and momentum, not the number (e.g. \"easing down steadily\", \"holding right where it's settled\") — then bring in ONE concrete number from the snapshot right after as support, read as a continuous trend and pace against their own weekly/monthly baseline (pace per week, plateau, milestone — not the single value, and not the WHO band, which the BMI card covers); never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zum Gewicht dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — Richtung und Tempo, nicht der Zahl (z. B. \"geht ruhig nach unten\", \"hält sich genau da, wo es sich eingependelt hat\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, als kontinuierlichen Trend und Tempo gegen die eigene Wochen-/Monats-Baseline gelesen (Tempo pro Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt); führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", "", "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", "", - "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", - "", - "REPETITION: you have already given a similar \"no material change\" assessment for this metric 10 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 11 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", - "", - "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", - "- Higher systolic blood pressure looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=47, r=0.88)", - "- Higher sleep duration looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=45, r=0.98)", - "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", "", "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"your weight\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" - ] + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"weight\":{\"unit\":\"kg\",\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null},\"bloodPressureContext\":{\"systolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"diastolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"pairedDaily\":[]},\"weightVsSystolic\":{\"correlation\":null,\"pairs\":[]},\"weightVsMeanBloodPressure\":{\"correlation\":null,\"pairs\":[]},\"moodContext\":null}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "cached-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "forced-unchanged-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "next-day-en": { - "calls": [], - "enqueued": [], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z" - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "read-only-stale": { - "calls": [], - "enqueued": [ - { - "userId": "parity-rich", - "metric": "weight", - "locale": "en" - } - ], - "result": { - "hasProvider": true, - "text": "Parity note for fresh-en.", - "cached": true, - "updatedAt": "2026-06-16T10:00:00.000Z", - "preparing": false, - "revalidating": true - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - } - ] - }, - "prose-de": { - "calls": [ - { + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -356,88 +104,133 @@ "METRIK — GEWICHT:", "- Der Snapshot trägt weight.signal (der fertige Vergleich) + weight.summary + weight.series (graded). weight.latestDayFocus zeigt den jüngsten Tageswert, den Schritt zum vorherigen Messtag und ggf. den Blutdruck desselben Tages.", "- Fokus dieser Karte ist der kontinuierliche TREND und das TEMPO über die Zeit (Tempo pro Woche, Plateaus, Meilensteine), nicht die WHO-Band-Einordnung — die trägt die BMI-Karte. Den BMI-Wert nur als Nebenbezug nennen, nie das Band als zentrale Aussage hier.", - "- Wochen-/Monatstrend zählt, nicht der Einzelwert: Führe mit weight.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Tageschwankungen von 2.2 lb-4.4 lb (Wasser, Tageszeit) sind normal und kein Befund.", + "- Wochen-/Monatstrend zählt, nicht der Einzelwert: Führe mit weight.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Tageschwankungen von 1 kg-2 kg (Wasser, Tageszeit) sind normal und kein Befund.", "- Eigenes Ziel: Trägt der Snapshot weight.target, ist das der Zielbereich, den die Person selbst gesetzt hat; weight.target.progress sagt, welche Richtung Fortschritt ist (gaining = zunehmen, losing = abnehmen, holding steady = halten). Beurteile jede Veränderung GEGEN dieses Ziel: Liegt die Person darunter, ist eine Zunahme Fortschritt und nie ein Rückschritt. Die Hinweise zu Tempo und Meilensteinen beim Abnehmen unten gelten nur, wenn Abnehmen die Fortschrittsrichtung ist oder kein Ziel vorliegt.", - "- Plateau: Verändert sich das Mittel über > ~2 Wochen um < 1.1 lb, ist das ein Plateau — bei einem Abnehmenden ehrlich benennen, ohne zu entmutigen.", - "- Tempo: Nachhaltiger Verlust liegt bei ~1.1 lb-2.2 lb pro Woche (DGE). Anhaltend > 2.2 lb pro Woche ist eher Wasser/Muskel als Fett — vorsichtig einordnen.", + "- Plateau: Verändert sich das Mittel über > ~2 Wochen um < 0.5 kg, ist das ein Plateau — bei einem Abnehmenden ehrlich benennen, ohne zu entmutigen.", + "- Tempo: Nachhaltiger Verlust liegt bei ~0.5 kg-1 kg pro Woche (DGE). Anhaltend > 1 kg pro Woche ist eher Wasser/Muskel als Fett — vorsichtig einordnen.", "- Meilensteine gegen das eigene Maximum: ~5 % Verlust vom höchsten Wert (series-Max) bringt bereits metabolischen Nutzen, ~10 % einen deutlichen — wenn erreicht, ausdrücklich anerkennen.", "- BMI-Bezug nur als knapper Nebensatz, wenn die Größe im Profil vorliegt (sonst nicht behaupten); die WHO-Band-Einordnung selbst gehört auf die BMI-Karte, nicht hierher.", "- Zusammenhänge: weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation und moodContext.moodVsWeightCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. bei einem Plateau zur selben Tageszeit wiegen, um den echten Trend zu sehen, statt am Tagesrauschen zu hängen). Ist der Trend stabil und gibt es nichts Sinnvolles zu tun, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen." ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", - "Schreibe eine kurze Einschätzung zum Gewicht dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — Richtung und Tempo, nicht der Zahl (z. B. \"geht ruhig nach unten\", \"hält sich genau da, wo es sich eingependelt hat\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, als kontinuierlichen Trend und Tempo gegen die eigene Wochen-/Monats-Baseline gelesen (Tempo pro Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt); führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", - "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", - "", - "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", - "- Wenn die Schlafdauer höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=45, r=0.98)", - "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", - "", - "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"dein Gewicht\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": true, - "text": "Das ist kein JSON.", - "cached": false, - "updatedAt": "2026-06-15T10:00:00.000Z" - }, "notes": [ { - "userId": "parity-rich", - "metric": "weight", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "weight", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" }, { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Parity note for fresh-en.", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "items": null, + "locale": "de", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "weight", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "weight", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Für diese Karte liegt gerade keine Einschätzung vor. Gewicht liest sich als Verlauf, nicht als Tageszahl — unter konstanten Bedingungen gewogen, pendelt sich die normale Tagesschwankung von selbst ein. Die Richtung über Wochen, zusammen mit Blutdruck und BMI gelesen, ist das, was trägt.", + "updatedAt": null + } }, - "unparseable-fr": { + "bare-error-en": { "calls": [ { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's weight. Open with what the trend MEANS in plain words — the direction and momentum, not the number (e.g. \"easing down steadily\", \"holding right where it's settled\") — then bring in ONE concrete number from the snapshot right after as support, read as a continuous trend and pace against their own weekly/monthly baseline (pace per week, plateau, milestone — not the single value, and not the WHO band, which the BMI card covers); never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the single most recent value and where it sits — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"weight\":{\"unit\":\"kg\",\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null},\"bloodPressureContext\":{\"systolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"diastolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"pairedDaily\":[]},\"weightVsSystolic\":{\"correlation\":null,\"pairs\":[]},\"weightVsMeanBloodPressure\":{\"correlation\":null,\"pairs\":[]},\"moodContext\":null}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -511,26 +304,106 @@ "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", "", - "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", "{ \"summary\": \"...\" }", "", "METRIC — WEIGHT:", "- The snapshot carries weight.signal (the finished comparison) + weight.summary + weight.series (graded). weight.latestDayFocus shows the latest daily value, the step from the previous measured day and, where present, the same-day blood pressure.", "- This card's focus is the continuous TREND and PACE over time (pace per week, plateaus, milestones), not WHO-band placement — the BMI card carries that. Mention the BMI value only as a side reference, never make the band the central message here.", - "- The weekly/monthly trend matters, not the single value: lead from weight.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Day-to-day swings of 2.2 lb-4.4 lb (water, time of day) are normal and not a finding.", + "- The weekly/monthly trend matters, not the single value: lead from weight.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Day-to-day swings of 1 kg-2 kg (water, time of day) are normal and not a finding.", "- Own target: when the snapshot carries weight.target, that is the target band the person set themselves, and weight.target.progress says which way is progress (gaining, losing, or holding steady). Judge every change AGAINST that target: below it, a gain is progress and never a setback or a regression. The loss pace and milestone guidance below applies only when losing is the progress direction or no target is set.", - "- Plateau: if the mean moves < 1.1 lb over > ~2 weeks, that is a plateau — name it honestly for someone who is losing, without discouraging.", - "- Pace: sustainable loss is ~1.1 lb-2.2 lb per week (DGE). Sustained > 2.2 lb per week is more likely water/muscle than fat — frame cautiously.", + "- Plateau: if the mean moves < 0.5 kg over > ~2 weeks, that is a plateau — name it honestly for someone who is losing, without discouraging.", + "- Pace: sustainable loss is ~0.5 kg-1 kg per week (DGE). Sustained > 1 kg per week is more likely water/muscle than fat — frame cautiously.", "- Milestones against their own maximum: ~5% loss from the highest value (series max) already brings metabolic benefit, ~10% a substantial one — acknowledge explicitly when reached.", "- BMI reference only as a brief aside when height is in the profile (do not claim it otherwise); the WHO-band placement itself belongs on the BMI card, not here.", "- Associations: mention weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation and moodContext.moodVsWeightCorrelation only when present and |r| > 0.4 — as an association, never a cause.", - "- One message: close with ONE doable step that fits the direction ONLY when the finding implies one (e.g. on a plateau, weigh at the same time of day to see the real trend rather than the daily noise). When the trend is steady and there is nothing useful to do, affirm it honestly and name one thing worth keeping an eye on instead of manufacturing a step.", - "", - "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + "- One message: close with ONE doable step that fits the direction ONLY when the finding implies one (e.g. on a plateau, weigh at the same time of day to see the real trend rather than the daily noise). When the trend is steady and there is nothing useful to do, affirm it honestly and name one thing worth keeping an eye on instead of manufacturing a step." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "de", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "weight", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "weight", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. Weight reads as a trend rather than a daily number — weighed under consistent conditions, the normal day-to-day swing settles out on its own. The direction over weeks, read alongside blood pressure and BMI, is what carries.", + "updatedAt": null + } + }, + "blocked-es": { + "calls": [ + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", @@ -546,74 +419,18 @@ "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", "", "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Quand la pression systolique monte, le poids du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=47, r=0.88)", - "- Quand la durée de sommeil monte, le poids du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=45, r=0.98)", + "- Cuando la presión sistólica es mayor, el peso del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=47, r=0.88)", + "- Cuando la duración del sueño es mayor, el peso del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=45, r=0.98)", "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", "", "", - "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"dein Gewicht\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" - ] + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"dein Gewicht\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Un peu en dehors de tes repères habituels ces derniers temps. Ton poids est à 185,6 en ce moment — au-dessus de ta moyenne habituelle de 182,1. Pèse-toi à la même heure pendant quelques jours et la fluctuation quotidienne normale se lisse d’elle-même.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "weight", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "blocked-es": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -704,9 +521,128 @@ "", "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "de", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "weight", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "weight", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Últimamente algo fuera de tu rango habitual. Tu peso está en 185,6 ahora mismo — por encima de tu promedio habitual de 182,1. Pésate a la misma hora durante unos días y la fluctuación diaria normal se compensa sola.", + "updatedAt": null + } + }, + "cached-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "fresh-en": { + "calls": [ + { + "maxTokens": 250, "messages": [ { - "role": "user", "content": [ "Date: 2026-06-15", "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", @@ -715,94 +651,25 @@ "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", "", "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", "", - "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", "", - "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 10 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 11 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", "", - "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", - "- Cuando la presión sistólica es mayor, el peso del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=47, r=0.88)", - "- Cuando la duración del sueño es mayor, el peso del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=45, r=0.98)", - "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher systolic blood pressure looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=47, r=0.88)", + "- Higher sleep duration looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=45, r=0.98)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", "", "", - "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"dein Gewicht\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" - ] + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"your weight\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" + ], + "role": "user" } ], - "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, "responseFormat": "json", - "timeoutMs": 60000 - } - ], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Últimamente algo fuera de tu rango habitual. Tu peso está en 185,6 ahora mismo — por encima de tu promedio habitual de 182,1. Pésate a la misma hora durante unos días y la fluctuación diaria normal se compensa sola.", - "cached": true, - "updatedAt": null - }, - "notes": [ - { - "userId": "parity-rich", - "metric": "weight", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - } - ] - }, - "bare-error-en": { - "calls": [ - { + "seed": 1618033, "system": [ "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", "", @@ -882,120 +749,370 @@ "METRIC — WEIGHT:", "- The snapshot carries weight.signal (the finished comparison) + weight.summary + weight.series (graded). weight.latestDayFocus shows the latest daily value, the step from the previous measured day and, where present, the same-day blood pressure.", "- This card's focus is the continuous TREND and PACE over time (pace per week, plateaus, milestones), not WHO-band placement — the BMI card carries that. Mention the BMI value only as a side reference, never make the band the central message here.", - "- The weekly/monthly trend matters, not the single value: lead from weight.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Day-to-day swings of 1 kg-2 kg (water, time of day) are normal and not a finding.", + "- The weekly/monthly trend matters, not the single value: lead from weight.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Day-to-day swings of 2.2 lb-4.4 lb (water, time of day) are normal and not a finding.", "- Own target: when the snapshot carries weight.target, that is the target band the person set themselves, and weight.target.progress says which way is progress (gaining, losing, or holding steady). Judge every change AGAINST that target: below it, a gain is progress and never a setback or a regression. The loss pace and milestone guidance below applies only when losing is the progress direction or no target is set.", - "- Plateau: if the mean moves < 0.5 kg over > ~2 weeks, that is a plateau — name it honestly for someone who is losing, without discouraging.", - "- Pace: sustainable loss is ~0.5 kg-1 kg per week (DGE). Sustained > 1 kg per week is more likely water/muscle than fat — frame cautiously.", + "- Plateau: if the mean moves < 1.1 lb over > ~2 weeks, that is a plateau — name it honestly for someone who is losing, without discouraging.", + "- Pace: sustainable loss is ~1.1 lb-2.2 lb per week (DGE). Sustained > 2.2 lb per week is more likely water/muscle than fat — frame cautiously.", "- Milestones against their own maximum: ~5% loss from the highest value (series max) already brings metabolic benefit, ~10% a substantial one — acknowledge explicitly when reached.", "- BMI reference only as a brief aside when height is in the profile (do not claim it otherwise); the WHO-band placement itself belongs on the BMI card, not here.", "- Associations: mention weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation and moodContext.moodVsWeightCorrelation only when present and |r| > 0.4 — as an association, never a cause.", "- One message: close with ONE doable step that fits the direction ONLY when the finding implies one (e.g. on a plateau, weigh at the same time of day to see the real trend rather than the daily noise). When the trend is steady and there is nothing useful to do, affirm it honestly and name one thing worth keeping an eye on instead of manufacturing a step." ], - "messages": [ - { - "role": "user", - "content": [ - "Date: 2026-06-15", - "OPENER HINT: Open with what this means for the user today, then the number behind it.", - "Write one short assessment of this person's weight. Open with what the trend MEANS in plain words — the direction and momentum, not the number (e.g. \"easing down steadily\", \"holding right where it's settled\") — then bring in ONE concrete number from the snapshot right after as support, read as a continuous trend and pace against their own weekly/monthly baseline (pace per week, plateau, milestone — not the single value, and not the WHO band, which the BMI card covers); never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", - "", - "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", - "", - "", - "VARIETY: to keep your cards from reading the same, you may open this one by leading with the single most recent value and where it sits — only if the data genuinely supports that angle; never force it, never invent a trend.", - "", - "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", - "", - "", - "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"weight\":{\"unit\":\"kg\",\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null},\"bloodPressureContext\":{\"systolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"diastolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"pairedDaily\":[]},\"weightVsSystolic\":{\"correlation\":null,\"pairs\":[]},\"weightVsMeanBloodPressure\":{\"correlation\":null,\"pairs\":[]},\"moodContext\":null}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. Weight reads as a trend rather than a daily number — weighed under consistent conditions, the normal day-to-day swing settles out on its own. The direction over weeks, read alongside blood pressure and BMI, is what carries.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "weight", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "weight", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" }, { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Parity note for fresh-en.", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "items": null, + "locale": "de", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "weight", + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "weight", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "weight", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "weight", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Zuletzt etwas außerhalb deines üblichen Rahmens. Dein Gewicht liegt aktuell bei 84,2 — höher als dein üblicher Schnitt von 82,6. Wäge dich ein paar Tage zur gleichen Zeit, dann ordnet sich die normale Schwankung von selbst ein.", + "updatedAt": null + } }, - "bare-error-de": { + "keyless-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "weight", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "de", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "weight", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "weight", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "A little off your usual lately. Your weight is at 84.2 right now — higher than your usual average of 82.6. Weigh in at the same time for a few days and the normal day-to-day swing sorts itself out.", + "updatedAt": null + } + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "de", + "metric": "weight", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": "error", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-bare" + }, + { + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "de", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "text": "Das ist kein JSON.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "es", + "metric": "weight", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + }, + { + "dateKey": "2026-06-15", + "generatedAt": null, + "inputHash": null, + "items": null, + "locale": "fr", + "metric": "weight", + "negativeReason": "screened", + "retryAt": "2026-06-15T10:05:00.000Z", + "snapshotHash": null, + "text": null, + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "No assessment on this one right now. Weight reads as a trend rather than a daily number — weighed under consistent conditions, the normal day-to-day swing settles out on its own. The direction over weeks, read alongside blood pressure and BMI, is what carries.", + "updatedAt": null + } + }, + "next-day-en": { + "calls": [], + "enqueued": [], + "notes": [ + { + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, + "retryAt": null, + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" + } + ], + "result": { + "cached": true, + "hasProvider": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } + }, + "prose-de": { "calls": [ { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zum Gewicht dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — Richtung und Tempo, nicht der Zahl (z. B. \"geht ruhig nach unten\", \"hält sich genau da, wo es sich eingependelt hat\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, als kontinuierlichen Trend und Tempo gegen die eigene Wochen-/Monats-Baseline gelesen (Tempo pro Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt); führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", + "- Wenn die Schlafdauer höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=45, r=0.98)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"dein Gewicht\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, "system": [ "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", "", @@ -1075,398 +1192,281 @@ "METRIK — GEWICHT:", "- Der Snapshot trägt weight.signal (der fertige Vergleich) + weight.summary + weight.series (graded). weight.latestDayFocus zeigt den jüngsten Tageswert, den Schritt zum vorherigen Messtag und ggf. den Blutdruck desselben Tages.", "- Fokus dieser Karte ist der kontinuierliche TREND und das TEMPO über die Zeit (Tempo pro Woche, Plateaus, Meilensteine), nicht die WHO-Band-Einordnung — die trägt die BMI-Karte. Den BMI-Wert nur als Nebenbezug nennen, nie das Band als zentrale Aussage hier.", - "- Wochen-/Monatstrend zählt, nicht der Einzelwert: Führe mit weight.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Tageschwankungen von 1 kg-2 kg (Wasser, Tageszeit) sind normal und kein Befund.", + "- Wochen-/Monatstrend zählt, nicht der Einzelwert: Führe mit weight.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Tageschwankungen von 2.2 lb-4.4 lb (Wasser, Tageszeit) sind normal und kein Befund.", "- Eigenes Ziel: Trägt der Snapshot weight.target, ist das der Zielbereich, den die Person selbst gesetzt hat; weight.target.progress sagt, welche Richtung Fortschritt ist (gaining = zunehmen, losing = abnehmen, holding steady = halten). Beurteile jede Veränderung GEGEN dieses Ziel: Liegt die Person darunter, ist eine Zunahme Fortschritt und nie ein Rückschritt. Die Hinweise zu Tempo und Meilensteinen beim Abnehmen unten gelten nur, wenn Abnehmen die Fortschrittsrichtung ist oder kein Ziel vorliegt.", - "- Plateau: Verändert sich das Mittel über > ~2 Wochen um < 0.5 kg, ist das ein Plateau — bei einem Abnehmenden ehrlich benennen, ohne zu entmutigen.", - "- Tempo: Nachhaltiger Verlust liegt bei ~0.5 kg-1 kg pro Woche (DGE). Anhaltend > 1 kg pro Woche ist eher Wasser/Muskel als Fett — vorsichtig einordnen.", + "- Plateau: Verändert sich das Mittel über > ~2 Wochen um < 1.1 lb, ist das ein Plateau — bei einem Abnehmenden ehrlich benennen, ohne zu entmutigen.", + "- Tempo: Nachhaltiger Verlust liegt bei ~1.1 lb-2.2 lb pro Woche (DGE). Anhaltend > 2.2 lb pro Woche ist eher Wasser/Muskel als Fett — vorsichtig einordnen.", "- Meilensteine gegen das eigene Maximum: ~5 % Verlust vom höchsten Wert (series-Max) bringt bereits metabolischen Nutzen, ~10 % einen deutlichen — wenn erreicht, ausdrücklich anerkennen.", "- BMI-Bezug nur als knapper Nebensatz, wenn die Größe im Profil vorliegt (sonst nicht behaupten); die WHO-Band-Einordnung selbst gehört auf die BMI-Karte, nicht hierher.", "- Zusammenhänge: weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation und moodContext.moodVsWeightCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. bei einem Plateau zur selben Tageszeit wiegen, um den echten Trend zu sehen, statt am Tagesrauschen zu hängen). Ist der Trend stabil und gibt es nichts Sinnvolles zu tun, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen." ], - "messages": [ - { - "role": "user", - "content": [ - "Datum: 2026-06-15", - "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", - "Schreibe eine kurze Einschätzung zum Gewicht dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — Richtung und Tempo, nicht der Zahl (z. B. \"geht ruhig nach unten\", \"hält sich genau da, wo es sich eingependelt hat\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, als kontinuierlichen Trend und Tempo gegen die eigene Wochen-/Monats-Baseline gelesen (Tempo pro Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt); führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", - "", - "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", - "", - "", - "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", - "", - "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", - "", - "", - "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"weight\":{\"unit\":\"kg\",\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null},\"bloodPressureContext\":{\"systolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"diastolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"pairedDaily\":[]},\"weightVsSystolic\":{\"correlation\":null,\"pairs\":[]},\"weightVsMeanBloodPressure\":{\"correlation\":null,\"pairs\":[]},\"moodContext\":null}" - ] - } - ], "temperature": 0.45, - "maxTokens": 250, - "seed": 1618033, - "responseFormat": "json", "timeoutMs": 60000 } ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "Für diese Karte liegt gerade keine Einschätzung vor. Gewicht liest sich als Verlauf, nicht als Tageszahl — unter konstanten Bedingungen gewogen, pendelt sich die normale Tagesschwankung von selbst ein. Die Richtung über Wochen, zusammen mit Blutdruck und BMI gelesen, ist das, was trägt.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "weight", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "weight", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "de", + "metric": "weight", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" } - ] + ], + "result": { + "cached": false, + "hasProvider": true, + "text": "Das ist kein JSON.", + "updatedAt": "2026-06-15T10:00:00.000Z" + } }, - "keyless-en": { + "read-only-cold": { "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "A little off your usual lately. Your weight is at 84.2 right now — higher than your usual average of 82.6. Weigh in at the same time for a few days and the normal day-to-day swing sorts itself out.", - "cached": true, - "updatedAt": null - }, - "notes": [ + "enqueued": [ { - "userId": "parity-bare", - "metric": "weight", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "weight", "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", "metric": "weight", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", - "dateKey": "2026-06-16", - "generatedAt": "2026-06-16T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "userId": "parity-rich" } - ] + ], + "notes": [], + "result": { + "cached": false, + "hasProvider": true, + "preparing": true, + "revalidating": false, + "text": null, + "updatedAt": null + } }, - "keyless-de": { + "read-only-stale": { "calls": [], - "enqueued": [], - "result": { - "hasProvider": false, - "text": "Zuletzt etwas außerhalb deines üblichen Rahmens. Dein Gewicht liegt aktuell bei 84,2 — höher als dein üblicher Schnitt von 82,6. Wäge dich ein paar Tage zur gleichen Zeit, dann ordnet sich die normale Schwankung von selbst ein.", - "cached": true, - "updatedAt": null - }, + "enqueued": [ + { + "locale": "en", + "metric": "weight", + "userId": "parity-rich" + } + ], "notes": [ { - "userId": "parity-bare", - "metric": "weight", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "weight", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", - "dateKey": "2026-06-15", - "generatedAt": "2026-06-15T10:00:00.000Z", - "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, "retryAt": null, - "negativeReason": null - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": true, + "preparing": false, + "revalidating": true, + "text": "Parity note for fresh-en.", + "updatedAt": "2026-06-16T10:00:00.000Z" + } }, - "keyless-read-only": { - "calls": [], + "unparseable-fr": { + "calls": [ + { + "maxTokens": 250, + "messages": [ + { + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's weight. Open with what the trend MEANS in plain words — the direction and momentum, not the number (e.g. \"easing down steadily\", \"holding right where it's settled\") — then bring in ONE concrete number from the snapshot right after as support, read as a continuous trend and pace against their own weekly/monthly baseline (pace per week, plateau, milestone — not the single value, and not the WHO band, which the BMI card covers); never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Quand la pression systolique monte, le poids du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=47, r=0.88)", + "- Quand la durée de sommeil monte, le poids du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=45, r=0.98)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"dein Gewicht\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" + ], + "role": "user" + } + ], + "responseFormat": "json", + "seed": 1618033, + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — WEIGHT:", + "- The snapshot carries weight.signal (the finished comparison) + weight.summary + weight.series (graded). weight.latestDayFocus shows the latest daily value, the step from the previous measured day and, where present, the same-day blood pressure.", + "- This card's focus is the continuous TREND and PACE over time (pace per week, plateaus, milestones), not WHO-band placement — the BMI card carries that. Mention the BMI value only as a side reference, never make the band the central message here.", + "- The weekly/monthly trend matters, not the single value: lead from weight.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Day-to-day swings of 2.2 lb-4.4 lb (water, time of day) are normal and not a finding.", + "- Own target: when the snapshot carries weight.target, that is the target band the person set themselves, and weight.target.progress says which way is progress (gaining, losing, or holding steady). Judge every change AGAINST that target: below it, a gain is progress and never a setback or a regression. The loss pace and milestone guidance below applies only when losing is the progress direction or no target is set.", + "- Plateau: if the mean moves < 1.1 lb over > ~2 weeks, that is a plateau — name it honestly for someone who is losing, without discouraging.", + "- Pace: sustainable loss is ~1.1 lb-2.2 lb per week (DGE). Sustained > 2.2 lb per week is more likely water/muscle than fat — frame cautiously.", + "- Milestones against their own maximum: ~5% loss from the highest value (series max) already brings metabolic benefit, ~10% a substantial one — acknowledge explicitly when reached.", + "- BMI reference only as a brief aside when height is in the profile (do not claim it otherwise); the WHO-band placement itself belongs on the BMI card, not here.", + "- Associations: mention weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation and moodContext.moodVsWeightCorrelation only when present and |r| > 0.4 — as an association, never a cause.", + "- One message: close with ONE doable step that fits the direction ONLY when the finding implies one (e.g. on a plateau, weigh at the same time of day to see the real trend rather than the daily noise). When the trend is steady and there is nothing useful to do, affirm it honestly and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "temperature": 0.45, + "timeoutMs": 60000 + } + ], "enqueued": [], - "result": { - "hasProvider": false, - "text": "No assessment on this one right now. Weight reads as a trend rather than a daily number — weighed under consistent conditions, the normal day-to-day swing settles out on its own. The direction over weeks, read alongside blood pressure and BMI, is what carries.", - "cached": true, - "updatedAt": null - }, "notes": [ { - "userId": "parity-bare", - "metric": "weight", - "locale": "de", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-bare", - "metric": "weight", - "locale": "en", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "error" - }, - { - "userId": "parity-rich", - "metric": "weight", - "locale": "de", - "text": "Das ist kein JSON.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", "dateKey": "2026-06-15", "generatedAt": "2026-06-15T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "de", + "metric": "weight", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "text": "Das ist kein JSON.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "weight", - "locale": "en", - "text": "Parity note for fresh-en.", - "items": null, - "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", - "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", "dateKey": "2026-06-16", "generatedAt": "2026-06-16T10:00:00.000Z", + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "items": null, + "locale": "en", + "metric": "weight", + "negativeReason": null, "retryAt": null, - "negativeReason": null + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "text": "Parity note for fresh-en.", + "userId": "parity-rich" }, { - "userId": "parity-rich", - "metric": "weight", - "locale": "es", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, "dateKey": "2026-06-15", "generatedAt": null, - "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" - }, - { - "userId": "parity-rich", - "metric": "weight", + "inputHash": null, + "items": null, "locale": "fr", - "text": null, - "items": null, - "inputHash": null, - "snapshotHash": null, - "dateKey": "2026-06-15", - "generatedAt": null, + "metric": "weight", + "negativeReason": "screened", "retryAt": "2026-06-15T10:05:00.000Z", - "negativeReason": "screened" + "snapshotHash": null, + "text": null, + "userId": "parity-rich" } - ] + ], + "result": { + "cached": true, + "hasProvider": false, + "text": "Un peu en dehors de tes repères habituels ces derniers temps. Ton poids est à 185,6 en ce moment — au-dessus de ta moyenne habituelle de 182,1. Pèse-toi à la même heure pendant quelques jours et la fluctuation quotidienne normale se lisse d’elle-même.", + "updatedAt": null + } } } diff --git a/tests/integration/pulse-day-mean-method.test.ts b/tests/integration/pulse-day-mean-method.test.ts index 4dc463747..974d8c397 100644 --- a/tests/integration/pulse-day-mean-method.test.ts +++ b/tests/integration/pulse-day-mean-method.test.ts @@ -350,6 +350,13 @@ describe("a large account under the production heap", () => { const YEARS = 2; /** The production container's heap ceiling. */ const PROD_HEAP_MB = 524; + /** + * The seed's transactions run past Prisma's 5 s default under a loaded + * host: a month took at most 0.55 s on a quiet machine and 9.9 s at a load + * average near 95, which expired the transaction. The bound is the + * fixture's, not the code's under test. + */ + const SEED_TRANSACTION = { maxWait: 30_000, timeout: 120_000 }; beforeAll(async () => { await prisma.user.create({ @@ -359,9 +366,10 @@ describe("a large account under the production heap", () => { // seventh hour holding a single reading so the two methods disagree. // // Written a month at a time, each month in its own transaction with the - // statement timeout lifted for that transaction only: as one statement + // statement timeout lifted for that transaction only and a transaction + // timeout sized for a loaded host (`SEED_TRANSACTION`): as one statement // the million-row insert outran the timeout on CI's shared Postgres, and - // the timeout guards the code under test, not this fixture. + // both timeouts guard the code under test, not this fixture. const end = new Date(); end.setUTCHours(0, 0, 0, 0); const start = new Date(end.getTime() - YEARS * 365 * DAY); @@ -369,9 +377,9 @@ describe("a large account under the production heap", () => { for (let from = start.getTime(); from < end.getTime(); from += CHUNK) { const chunkFrom = new Date(from); const chunkTo = new Date(Math.min(from + CHUNK, end.getTime())); - await prisma.$transaction([ - prisma.$executeRaw`SET LOCAL statement_timeout = 0`, - prisma.$executeRaw` + await prisma.$transaction(async (tx) => { + await tx.$executeRaw`SET LOCAL statement_timeout = 0`; + await tx.$executeRaw` INSERT INTO measurements (id, user_id, type, unit, source, value, measured_at, created_at, updated_at) SELECT 'big-' || (extract(epoch FROM ts)::bigint / 60)::text, ${BIG}, 'PULSE', 'bpm', 'APPLE_HEALTH', 60 + ((extract(epoch FROM ts)::bigint / 60) % 40), @@ -382,13 +390,13 @@ describe("a large account under the production heap", () => { interval '1 minute' ) AS s(ts) WHERE extract(hour FROM ts)::int % 7 <> 3 OR extract(minute FROM ts)::int = 0 - `, - ]); + `; + }, SEED_TRANSACTION); } - await prisma.$transaction([ - prisma.$executeRaw`SET LOCAL statement_timeout = 0`, - prisma.$executeRaw`ANALYZE measurements`, - ]); + await prisma.$transaction(async (tx) => { + await tx.$executeRaw`SET LOCAL statement_timeout = 0`; + await tx.$executeRaw`ANALYZE measurements`; + }, SEED_TRANSACTION); }, 600_000); async function peakHeapMb(work: () => Promise) { diff --git a/tests/integration/routes/kit-contract.test.ts b/tests/integration/routes/kit-contract.test.ts index 7359258c1..d2e28b609 100644 --- a/tests/integration/routes/kit-contract.test.ts +++ b/tests/integration/routes/kit-contract.test.ts @@ -12,7 +12,12 @@ import { describe, expect, it } from "vitest"; import { apiHandler } from "@/lib/api-handler"; -import { ContractViolation, anonymous, callRoute } from "../../kit/call-route"; +import { + ContractViolation, + anonymous, + callRoute, + sealedFieldPaths, +} from "../../kit/call-route"; const token = { id: "tok-1", @@ -28,6 +33,31 @@ function answering(status: number, body: unknown) { return apiHandler(async () => NextResponse.json(body, { status })); } +describe("callRoute's sealed-field check", () => { + it("finds a sealed key and serialised bytes at any depth, and nothing else", () => { + const bytes = Object.fromEntries( + Array.from({ length: 32 }, (_, i) => [String(i), (i * 7) % 256]), + ); + expect( + sealedFieldPaths({ + data: { + rows: [{ notes: "a", notesEncrypted: null }], + track: { geometry: bytes }, + raw: { type: "Buffer", data: [1, 2, 3] }, + hours: { "0": 3, "1": 4, "2": 5 }, + }, + }), + ).toEqual([ + "data.rows[0].notesEncrypted", + "data.track.geometry", + "data.raw", + ]); + expect(sealedFieldPaths({ data: { notes: "plain", count: 3 } })).toEqual( + [], + ); + }); +}); + describe("callRoute's contract check", () => { it("accepts a body the registered schema describes", async () => { const res = await callRoute( diff --git a/tests/integration/routes/measurements-no-ciphertext.test.ts b/tests/integration/routes/measurements-no-ciphertext.test.ts new file mode 100644 index 000000000..702e6a688 --- /dev/null +++ b/tests/integration/routes/measurements-no-ciphertext.test.ts @@ -0,0 +1,108 @@ +/** + * No measurement read sends a sealed column to the client. + * + * A measurement's note is stored sealed in `notesEncrypted` and answered + * decrypted on `notes`. The `dayKey` drill-down of `GET /api/measurements` + * once answered the stored rows as they were, ciphertext included. Every + * read below runs over rows that carry a sealed note, and each must answer + * the readable note and no `*Encrypted` field or raw bytes. `callRoute` + * refuses sealed fields on every route it drives; the explicit checks here + * say which reads were looked at. + */ +import { beforeEach, describe, expect, it } from "vitest"; + +import { GET as listMeasurements } from "@/app/api/measurements/route"; +import { GET as readMeasurement } from "@/app/api/measurements/[id]/route"; +import { encryptNote } from "@/lib/crypto/note-cipher"; + +import { callRoute, sealedFieldPaths } from "../../kit/call-route"; +import { + makeMeasurement, + makeSession, + makeUser, + type SessionFixture, +} from "../../kit/factories"; +import { getPrismaClient, truncateAllTables } from "../setup"; + +const NOTE = "felt dizzy after standing up"; + +let session: SessionFixture; +let weightId: string; + +beforeEach(async () => { + await truncateAllTables(getPrismaClient()); + const user = await makeUser({ timezone: "UTC" }); + session = await makeSession(user); + const weight = await makeMeasurement(user, { + notes: null, + notesEncrypted: encryptNote(NOTE), + }); + weightId = weight.id; + await makeMeasurement(user, { + type: "ACTIVITY_STEPS", + value: 420, + unit: "steps", + source: "APPLE_HEALTH", + measuredAt: new Date("2026-03-02T09:00:00Z"), + externalId: "uuid-steps-1", + notes: null, + notesEncrypted: encryptNote(NOTE), + }); +}); + +type Row = { notes?: string | null }; + +async function list(query: Record) { + return callRoute<{ measurements: Row[] }>(listMeasurements, { + method: "GET", + route: "/api/measurements", + query, + auth: session, + }); +} + +describe("measurement reads answer the note, never its ciphertext", () => { + it("the plain page", async () => { + const res = await list({ limit: 10 }); + expect(res.status).toBe(200); + expect(sealedFieldPaths(res.body)).toEqual([]); + expect(res.data.measurements.map((m) => m.notes)).toEqual([NOTE, NOTE]); + }); + + it("the cumulative day drill-down", async () => { + const res = await list({ type: "ACTIVITY_STEPS", dayKey: "2026-03-02" }); + expect(res.status).toBe(200); + expect(sealedFieldPaths(res.body)).toEqual([]); + expect(res.data.measurements).toHaveLength(1); + expect(res.data.measurements[0].notes).toBe(NOTE); + }); + + it("the day-grouped list and an aggregate", async () => { + const queries: Array> = [ + { type: "ACTIVITY_STEPS", groupBy: "day" }, + { + type: "WEIGHT", + aggregate: "daily", + from: "2026-03-01T00:00:00.000Z", + to: "2026-03-03T00:00:00.000Z", + }, + ]; + for (const query of queries) { + const res = await list(query); + expect(res.status).toBe(200); + expect(sealedFieldPaths(res.body)).toEqual([]); + } + }); + + it("the detail read", async () => { + const res = await callRoute(readMeasurement, { + method: "GET", + route: "/api/measurements/{id}", + params: { id: weightId }, + auth: session, + }); + expect(res.status).toBe(200); + expect(sealedFieldPaths(res.body)).toEqual([]); + expect(res.data.notes).toBe(NOTE); + }); +}); diff --git a/tests/integration/routes/parity/__snapshots__/analytics.01-invalid-slice.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.01-invalid-slice.json.snap new file mode 100644 index 000000000..d926ee8f1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/analytics.01-invalid-slice.json.snap @@ -0,0 +1,39 @@ +{ + "status": 422, + "cacheControl": null, + "body": { + "data": null, + "error": "Validation failed", + "details": { + "issues": [ + { + "path": "slice", + "code": "invalid_value", + "message": "Invalid input: expected \"summaries\"" + } + ] + }, + "meta": { + "errorCode": "analytics.invalid_query", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "analytics.get" + }, + "meta": { + "outcome": "invalid_query" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "healthScores": [], + "patterns": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/analytics.02-summaries-cold.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.02-summaries-cold.json.snap new file mode 100644 index 000000000..03b0f8029 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/analytics.02-summaries-cold.json.snap @@ -0,0 +1,1463 @@ +{ + "status": 200, + "cacheControl": "private, max-age=0, must-revalidate", + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 60, + "latest": 82, + "min": 82, + "max": 84.4, + "mean": 83.18, + "median": 83.2, + "avg7": 82.11, + "avg30": 82.58, + "slope7": { + "slope": -0.042, + "direction": "down", + "confidence": 0.93 + }, + "slope30": { + "slope": -0.04, + "direction": "down", + "confidence": 0.99 + }, + "avg30LastMonth": 83.78, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 91, + "latest": 123, + "min": 123, + "max": 137, + "mean": 129.92, + "median": 130, + "avg7": 131.14, + "avg30": 130, + "slope7": { + "slope": -0.393, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": 0.071, + "direction": "up", + "confidence": 0.02 + }, + "avg30LastMonth": 130, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 91, + "latest": 78, + "min": 78, + "max": 86, + "mean": 81.96, + "median": 82, + "avg7": 81.43, + "avg30": 81.8, + "slope7": { + "slope": -0.714, + "direction": "down", + "confidence": 0.36 + }, + "slope30": { + "slope": -0.017, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": 82.1, + "avg30LastYear": null + }, + "PULSE": { + "count": 60, + "latest": 64, + "min": 64, + "max": 69, + "mean": 66.5, + "median": 66.5, + "avg7": 66.14, + "avg30": 66.5, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": -0.02, + "direction": "down", + "confidence": 0.01 + }, + "avg30LastMonth": 66.5, + "avg30LastYear": null + }, + "BODY_FAT": { + "count": 9, + "latest": 24, + "min": 24, + "max": 25.1, + "mean": 24.56, + "median": 24.6, + "avg7": 24, + "avg30": 24.28, + "slope7": { + "slope": -0.014, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.021, + "direction": "down", + "confidence": 0.99 + }, + "avg30LastMonth": 24.9, + "avg30LastYear": null + }, + "SLEEP_DURATION": { + "count": 21, + "latest": 355, + "min": 355, + "max": 430, + "mean": 387.86, + "median": 390, + "avg7": 386.43, + "avg30": 387.86, + "slope7": { + "slope": -0.1, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.03, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVITY_STEPS": { + "count": 30, + "latest": 6000, + "min": 6000, + "max": 9833, + "mean": 7808.5, + "median": 7781.5, + "avg7": 7119, + "avg30": 7808.5, + "slope7": { + "slope": -373, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -30.397, + "direction": "down", + "confidence": 0.06 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BLOOD_GLUCOSE": { + "count": 60, + "latest": 128, + "min": 92, + "max": 147, + "mean": 112.95, + "median": 106.5, + "avg7": 112.76, + "avg30": 112.95, + "slope7": { + "slope": -0.424, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.123, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TOTAL_BODY_WATER": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BONE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "OXYGEN_SATURATION": { + "count": 10, + "latest": 96, + "min": 96, + "max": 98, + "mean": 96.9, + "median": 97, + "avg7": 96.86, + "avg30": 96.9, + "slope7": { + "slope": -0.036, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.018, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HEART_RATE_VARIABILITY": { + "count": 30, + "latest": 45, + "min": 45, + "max": 52, + "mean": 48.3, + "median": 48, + "avg7": 48, + "avg30": 48.3, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.032, + "direction": "down", + "confidence": 0.02 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESTING_HEART_RATE": { + "count": 40, + "latest": 58, + "min": 58, + "max": 61, + "mean": 59.5, + "median": 59.5, + "avg7": 59.29, + "avg30": 59.43, + "slope7": { + "slope": -0.238, + "direction": "down", + "confidence": 0.24 + }, + "slope30": { + "slope": -0.006, + "direction": "stable", + "confidence": 0 + }, + "avg30LastMonth": 59.7, + "avg30LastYear": null + }, + "ACTIVE_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FLIGHTS_CLIMBED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_RUNNING_DISTANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VO2_MAX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_FREE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MUSCLE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SKIN_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PULSE_WAVE_VELOCITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VASCULAR_AGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VISCERAL_FAT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_ENV": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_HEADPHONE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TIME_IN_DAYLIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESPIRATORY_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_MASS_INDEX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LEAN_BODY_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_HEART_RATE_AVERAGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_ASYMMETRY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_DOUBLE_SUPPORT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEP_LENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "CARDIO_RECOVERY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WRIST_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FALL_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SIX_MINUTE_WALK_DISTANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAIR_ASCENT_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAIR_DESCENT_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BREATHING_DISTURBANCES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "IRREGULAR_RHYTHM_NOTIFICATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HIGH_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LOW_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BREATHING_DISTURBANCE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RECOVERY_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRESS_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRAIN_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HRV_RMSSD": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "DAY_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WORKOUT_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_PERFORMANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_EFFICIENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_CONSISTENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_NEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ENERGY_EXPENDITURE_KJ": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AVERAGE_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MAX_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_DISTURBANCE_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ANS_CHARGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "CARDIO_LOAD": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE_DEVIATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESILIENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PHQ9_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GAD7_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GRIP_STRENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PAIN_NRS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_CIRCUMFERENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_TO_HEIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WHO5_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SCI_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "EXERCISE_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_HOURS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MINDFUL_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STATE_OF_MIND_VALENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AFIB_BURDEN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_CO2": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_HUMIDITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + } + }, + "bmi": null, + "lastSeenByType": { + "WEIGHT": { + "lastSeenAt": "anchor+0d+06:30:00", + "daysAgo": 0 + }, + "BLOOD_PRESSURE_SYS": { + "lastSeenAt": "anchor+0d+07:00:00", + "daysAgo": 0 + }, + "BLOOD_PRESSURE_DIA": { + "lastSeenAt": "anchor+0d+07:00:00", + "daysAgo": 0 + }, + "PULSE": { + "lastSeenAt": "anchor+0d+07:01:00", + "daysAgo": 0 + }, + "BODY_FAT": { + "lastSeenAt": "anchor+0d+06:31:00", + "daysAgo": 0 + }, + "SLEEP_DURATION": { + "lastSeenAt": "anchor+0d+04:15:00", + "daysAgo": 0 + }, + "ACTIVITY_STEPS": { + "lastSeenAt": "anchor+0d+09:00:00", + "daysAgo": 0 + }, + "BLOOD_GLUCOSE": { + "lastSeenAt": "anchor+0d+11:00:00", + "daysAgo": 0 + }, + "TOTAL_BODY_WATER": null, + "BONE_MASS": null, + "OXYGEN_SATURATION": { + "lastSeenAt": "anchor+0d+04:40:00", + "daysAgo": 0 + }, + "HEART_RATE_VARIABILITY": { + "lastSeenAt": "anchor+0d+04:30:00", + "daysAgo": 0 + }, + "RESTING_HEART_RATE": { + "lastSeenAt": "anchor+0d+05:00:00", + "daysAgo": 0 + }, + "ACTIVE_ENERGY_BURNED": null, + "FLIGHTS_CLIMBED": null, + "WALKING_RUNNING_DISTANCE": null, + "VO2_MAX": null, + "BODY_TEMPERATURE": null, + "FAT_FREE_MASS": null, + "FAT_MASS": null, + "MUSCLE_MASS": null, + "SKIN_TEMPERATURE": null, + "PULSE_WAVE_VELOCITY": null, + "VASCULAR_AGE": null, + "VISCERAL_FAT": null, + "AUDIO_EXPOSURE_ENV": null, + "AUDIO_EXPOSURE_HEADPHONE": null, + "TIME_IN_DAYLIGHT": null, + "WALKING_STEADINESS": null, + "AUDIO_EXPOSURE_EVENT": null, + "RESPIRATORY_RATE": null, + "BODY_MASS_INDEX": null, + "LEAN_BODY_MASS": null, + "WALKING_HEART_RATE_AVERAGE": null, + "WALKING_ASYMMETRY": null, + "WALKING_DOUBLE_SUPPORT": null, + "WALKING_STEP_LENGTH": null, + "WALKING_SPEED": null, + "CARDIO_RECOVERY": null, + "WRIST_TEMPERATURE": null, + "FALL_COUNT": null, + "SIX_MINUTE_WALK_DISTANCE": null, + "STAIR_ASCENT_SPEED": null, + "STAIR_DESCENT_SPEED": null, + "BREATHING_DISTURBANCES": null, + "IRREGULAR_RHYTHM_NOTIFICATION": null, + "HIGH_HEART_RATE_EVENT": null, + "LOW_HEART_RATE_EVENT": null, + "WALKING_STEADINESS_EVENT": null, + "BREATHING_DISTURBANCE_EVENT": null, + "RECOVERY_SCORE": null, + "STRESS_SCORE": null, + "STRAIN_SCORE": null, + "HRV_RMSSD": null, + "DAY_STRAIN": null, + "WORKOUT_STRAIN": null, + "SLEEP_PERFORMANCE": null, + "SLEEP_EFFICIENCY": null, + "SLEEP_CONSISTENCY": null, + "SLEEP_NEED": null, + "ENERGY_EXPENDITURE_KJ": null, + "AVERAGE_HEART_RATE": null, + "MAX_HEART_RATE": null, + "SLEEP_DISTURBANCE_COUNT": null, + "ANS_CHARGE": null, + "CARDIO_LOAD": null, + "SLEEP_SCORE": null, + "BODY_TEMPERATURE_DEVIATION": null, + "RESILIENCE": null, + "PHQ9_SCORE": null, + "GAD7_SCORE": null, + "GRIP_STRENGTH": null, + "PAIN_NRS": null, + "WAIST_CIRCUMFERENCE": null, + "WAIST_TO_HEIGHT": null, + "WHO5_SCORE": null, + "SCI_SCORE": null, + "EXERCISE_MINUTES": null, + "STAND_MINUTES": null, + "STAND_HOURS": null, + "MINDFUL_MINUTES": null, + "STATE_OF_MIND_VALENCE": null, + "AFIB_BURDEN": null, + "INDOOR_CO2": null, + "INDOOR_TEMPERATURE": null, + "INDOOR_HUMIDITY": null + }, + "sleepSourceDiscrepancy": null, + "recoveryEmpty": "general" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "analytics.get.slim" + }, + "meta": { + "analytics": { + "slim_summaries": { + "row_count": 566, + "type_count": 11, + "path": "rollup", + "year_over_year_types": 0, + "slope_source": "rollup", + "median_source": "live", + "regression_source": "rollup" + } + }, + "cache.summaries_slice.outcome": "miss" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserRollupsFresh", + "args": [ + "" + ] + } + ], + "stored": { + "healthScores": [], + "patterns": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/analytics.03-summaries-warm.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.03-summaries-warm.json.snap new file mode 100644 index 000000000..9eaff45da --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/analytics.03-summaries-warm.json.snap @@ -0,0 +1,1445 @@ +{ + "status": 200, + "cacheControl": "private, max-age=0, must-revalidate", + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 60, + "latest": 82, + "min": 82, + "max": 84.4, + "mean": 83.18, + "median": 83.2, + "avg7": 82.11, + "avg30": 82.58, + "slope7": { + "slope": -0.042, + "direction": "down", + "confidence": 0.93 + }, + "slope30": { + "slope": -0.04, + "direction": "down", + "confidence": 0.99 + }, + "avg30LastMonth": 83.78, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 91, + "latest": 123, + "min": 123, + "max": 137, + "mean": 129.92, + "median": 130, + "avg7": 131.14, + "avg30": 130, + "slope7": { + "slope": -0.393, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": 0.071, + "direction": "up", + "confidence": 0.02 + }, + "avg30LastMonth": 130, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 91, + "latest": 78, + "min": 78, + "max": 86, + "mean": 81.96, + "median": 82, + "avg7": 81.43, + "avg30": 81.8, + "slope7": { + "slope": -0.714, + "direction": "down", + "confidence": 0.36 + }, + "slope30": { + "slope": -0.017, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": 82.1, + "avg30LastYear": null + }, + "PULSE": { + "count": 60, + "latest": 64, + "min": 64, + "max": 69, + "mean": 66.5, + "median": 66.5, + "avg7": 66.14, + "avg30": 66.5, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": -0.02, + "direction": "down", + "confidence": 0.01 + }, + "avg30LastMonth": 66.5, + "avg30LastYear": null + }, + "BODY_FAT": { + "count": 9, + "latest": 24, + "min": 24, + "max": 25.1, + "mean": 24.56, + "median": 24.6, + "avg7": 24, + "avg30": 24.28, + "slope7": { + "slope": -0.014, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.021, + "direction": "down", + "confidence": 0.99 + }, + "avg30LastMonth": 24.9, + "avg30LastYear": null + }, + "SLEEP_DURATION": { + "count": 21, + "latest": 355, + "min": 355, + "max": 430, + "mean": 387.86, + "median": 390, + "avg7": 386.43, + "avg30": 387.86, + "slope7": { + "slope": -0.1, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.03, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVITY_STEPS": { + "count": 30, + "latest": 6000, + "min": 6000, + "max": 9833, + "mean": 7808.5, + "median": 7781.5, + "avg7": 7119, + "avg30": 7808.5, + "slope7": { + "slope": -373, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -30.397, + "direction": "down", + "confidence": 0.06 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BLOOD_GLUCOSE": { + "count": 60, + "latest": 128, + "min": 92, + "max": 147, + "mean": 112.95, + "median": 106.5, + "avg7": 112.76, + "avg30": 112.95, + "slope7": { + "slope": -0.424, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.123, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TOTAL_BODY_WATER": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BONE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "OXYGEN_SATURATION": { + "count": 10, + "latest": 96, + "min": 96, + "max": 98, + "mean": 96.9, + "median": 97, + "avg7": 96.86, + "avg30": 96.9, + "slope7": { + "slope": -0.036, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.018, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HEART_RATE_VARIABILITY": { + "count": 30, + "latest": 45, + "min": 45, + "max": 52, + "mean": 48.3, + "median": 48, + "avg7": 48, + "avg30": 48.3, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.032, + "direction": "down", + "confidence": 0.02 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESTING_HEART_RATE": { + "count": 40, + "latest": 58, + "min": 58, + "max": 61, + "mean": 59.5, + "median": 59.5, + "avg7": 59.29, + "avg30": 59.43, + "slope7": { + "slope": -0.238, + "direction": "down", + "confidence": 0.24 + }, + "slope30": { + "slope": -0.006, + "direction": "stable", + "confidence": 0 + }, + "avg30LastMonth": 59.7, + "avg30LastYear": null + }, + "ACTIVE_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FLIGHTS_CLIMBED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_RUNNING_DISTANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VO2_MAX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_FREE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MUSCLE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SKIN_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PULSE_WAVE_VELOCITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VASCULAR_AGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VISCERAL_FAT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_ENV": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_HEADPHONE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TIME_IN_DAYLIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESPIRATORY_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_MASS_INDEX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LEAN_BODY_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_HEART_RATE_AVERAGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_ASYMMETRY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_DOUBLE_SUPPORT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEP_LENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "CARDIO_RECOVERY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WRIST_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FALL_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SIX_MINUTE_WALK_DISTANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAIR_ASCENT_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAIR_DESCENT_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BREATHING_DISTURBANCES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "IRREGULAR_RHYTHM_NOTIFICATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HIGH_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LOW_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BREATHING_DISTURBANCE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RECOVERY_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRESS_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRAIN_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HRV_RMSSD": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "DAY_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WORKOUT_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_PERFORMANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_EFFICIENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_CONSISTENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_NEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ENERGY_EXPENDITURE_KJ": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AVERAGE_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MAX_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_DISTURBANCE_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ANS_CHARGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "CARDIO_LOAD": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE_DEVIATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESILIENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PHQ9_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GAD7_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GRIP_STRENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PAIN_NRS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_CIRCUMFERENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_TO_HEIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WHO5_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SCI_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "EXERCISE_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_HOURS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MINDFUL_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STATE_OF_MIND_VALENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AFIB_BURDEN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_CO2": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_HUMIDITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + } + }, + "bmi": null, + "lastSeenByType": { + "WEIGHT": { + "lastSeenAt": "anchor+0d+06:30:00", + "daysAgo": 0 + }, + "BLOOD_PRESSURE_SYS": { + "lastSeenAt": "anchor+0d+07:00:00", + "daysAgo": 0 + }, + "BLOOD_PRESSURE_DIA": { + "lastSeenAt": "anchor+0d+07:00:00", + "daysAgo": 0 + }, + "PULSE": { + "lastSeenAt": "anchor+0d+07:01:00", + "daysAgo": 0 + }, + "BODY_FAT": { + "lastSeenAt": "anchor+0d+06:31:00", + "daysAgo": 0 + }, + "SLEEP_DURATION": { + "lastSeenAt": "anchor+0d+04:15:00", + "daysAgo": 0 + }, + "ACTIVITY_STEPS": { + "lastSeenAt": "anchor+0d+09:00:00", + "daysAgo": 0 + }, + "BLOOD_GLUCOSE": { + "lastSeenAt": "anchor+0d+11:00:00", + "daysAgo": 0 + }, + "TOTAL_BODY_WATER": null, + "BONE_MASS": null, + "OXYGEN_SATURATION": { + "lastSeenAt": "anchor+0d+04:40:00", + "daysAgo": 0 + }, + "HEART_RATE_VARIABILITY": { + "lastSeenAt": "anchor+0d+04:30:00", + "daysAgo": 0 + }, + "RESTING_HEART_RATE": { + "lastSeenAt": "anchor+0d+05:00:00", + "daysAgo": 0 + }, + "ACTIVE_ENERGY_BURNED": null, + "FLIGHTS_CLIMBED": null, + "WALKING_RUNNING_DISTANCE": null, + "VO2_MAX": null, + "BODY_TEMPERATURE": null, + "FAT_FREE_MASS": null, + "FAT_MASS": null, + "MUSCLE_MASS": null, + "SKIN_TEMPERATURE": null, + "PULSE_WAVE_VELOCITY": null, + "VASCULAR_AGE": null, + "VISCERAL_FAT": null, + "AUDIO_EXPOSURE_ENV": null, + "AUDIO_EXPOSURE_HEADPHONE": null, + "TIME_IN_DAYLIGHT": null, + "WALKING_STEADINESS": null, + "AUDIO_EXPOSURE_EVENT": null, + "RESPIRATORY_RATE": null, + "BODY_MASS_INDEX": null, + "LEAN_BODY_MASS": null, + "WALKING_HEART_RATE_AVERAGE": null, + "WALKING_ASYMMETRY": null, + "WALKING_DOUBLE_SUPPORT": null, + "WALKING_STEP_LENGTH": null, + "WALKING_SPEED": null, + "CARDIO_RECOVERY": null, + "WRIST_TEMPERATURE": null, + "FALL_COUNT": null, + "SIX_MINUTE_WALK_DISTANCE": null, + "STAIR_ASCENT_SPEED": null, + "STAIR_DESCENT_SPEED": null, + "BREATHING_DISTURBANCES": null, + "IRREGULAR_RHYTHM_NOTIFICATION": null, + "HIGH_HEART_RATE_EVENT": null, + "LOW_HEART_RATE_EVENT": null, + "WALKING_STEADINESS_EVENT": null, + "BREATHING_DISTURBANCE_EVENT": null, + "RECOVERY_SCORE": null, + "STRESS_SCORE": null, + "STRAIN_SCORE": null, + "HRV_RMSSD": null, + "DAY_STRAIN": null, + "WORKOUT_STRAIN": null, + "SLEEP_PERFORMANCE": null, + "SLEEP_EFFICIENCY": null, + "SLEEP_CONSISTENCY": null, + "SLEEP_NEED": null, + "ENERGY_EXPENDITURE_KJ": null, + "AVERAGE_HEART_RATE": null, + "MAX_HEART_RATE": null, + "SLEEP_DISTURBANCE_COUNT": null, + "ANS_CHARGE": null, + "CARDIO_LOAD": null, + "SLEEP_SCORE": null, + "BODY_TEMPERATURE_DEVIATION": null, + "RESILIENCE": null, + "PHQ9_SCORE": null, + "GAD7_SCORE": null, + "GRIP_STRENGTH": null, + "PAIN_NRS": null, + "WAIST_CIRCUMFERENCE": null, + "WAIST_TO_HEIGHT": null, + "WHO5_SCORE": null, + "SCI_SCORE": null, + "EXERCISE_MINUTES": null, + "STAND_MINUTES": null, + "STAND_HOURS": null, + "MINDFUL_MINUTES": null, + "STATE_OF_MIND_VALENCE": null, + "AFIB_BURDEN": null, + "INDOOR_CO2": null, + "INDOOR_TEMPERATURE": null, + "INDOOR_HUMIDITY": null + }, + "sleepSourceDiscrepancy": null, + "recoveryEmpty": "general" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "analytics.get" + }, + "meta": { + "cache.summaries_slice.outcome": "hit" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "healthScores": [], + "patterns": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/analytics.04-default-cold.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.04-default-cold.json.snap new file mode 100644 index 000000000..013125ba9 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/analytics.04-default-cold.json.snap @@ -0,0 +1,2508 @@ +{ + "status": 200, + "cacheControl": "private, max-age=0, must-revalidate", + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 60, + "latest": 82, + "min": 82, + "max": 84.4, + "mean": 83.18, + "median": 83.2, + "avg7": 82.11, + "avg30": 82.58, + "slope7": { + "slope": -0.042, + "direction": "down", + "confidence": 0.93 + }, + "slope30": { + "slope": -0.04, + "direction": "down", + "confidence": 0.99 + }, + "avg30LastMonth": 83.78, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 91, + "latest": 123, + "min": 123, + "max": 137, + "mean": 129.92, + "median": 130, + "avg7": 131.14, + "avg30": 130, + "slope7": { + "slope": -0.393, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": 0.071, + "direction": "up", + "confidence": 0.02 + }, + "avg30LastMonth": 130, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 91, + "latest": 78, + "min": 78, + "max": 86, + "mean": 81.96, + "median": 82, + "avg7": 81.43, + "avg30": 81.8, + "slope7": { + "slope": -0.714, + "direction": "down", + "confidence": 0.36 + }, + "slope30": { + "slope": -0.017, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": 82.1, + "avg30LastYear": null + }, + "PULSE": { + "count": 60, + "latest": 64, + "min": 64, + "max": 69, + "mean": 66.5, + "median": 66.5, + "avg7": 66.14, + "avg30": 66.5, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": -0.02, + "direction": "down", + "confidence": 0.01 + }, + "avg30LastMonth": 66.5, + "avg30LastYear": null + }, + "BODY_FAT": { + "count": 9, + "latest": 24, + "min": 24, + "max": 25.1, + "mean": 24.56, + "median": 24.6, + "avg7": 24, + "avg30": 24.28, + "slope7": { + "slope": -0.014, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.021, + "direction": "down", + "confidence": 0.99 + }, + "avg30LastMonth": 24.9, + "avg30LastYear": null + }, + "SLEEP_DURATION": { + "count": 21, + "latest": 355, + "min": 355, + "max": 430, + "mean": 387.86, + "median": 390, + "avg7": 386.43, + "avg30": 387.86, + "slope7": { + "slope": -0.1, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.03, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVITY_STEPS": { + "count": 30, + "latest": 6000, + "min": 6000, + "max": 9833, + "mean": 7808.5, + "median": 7781.5, + "avg7": 7119, + "avg30": 7808.5, + "slope7": { + "slope": -373, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -30.397, + "direction": "down", + "confidence": 0.06 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BLOOD_GLUCOSE": { + "count": 60, + "latest": 128, + "min": 92, + "max": 147, + "mean": 112.95, + "median": 106.5, + "avg7": 112.76, + "avg30": 112.95, + "slope7": { + "slope": -0.424, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.123, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TOTAL_BODY_WATER": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BONE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "OXYGEN_SATURATION": { + "count": 10, + "latest": 96, + "min": 96, + "max": 98, + "mean": 96.9, + "median": 97, + "avg7": 96.86, + "avg30": 96.9, + "slope7": { + "slope": -0.036, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.018, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HEART_RATE_VARIABILITY": { + "count": 30, + "latest": 45, + "min": 45, + "max": 52, + "mean": 48.3, + "median": 48, + "avg7": 48, + "avg30": 48.3, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.032, + "direction": "down", + "confidence": 0.02 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESTING_HEART_RATE": { + "count": 40, + "latest": 58, + "min": 58, + "max": 61, + "mean": 59.5, + "median": 59.5, + "avg7": 59.29, + "avg30": 59.43, + "slope7": { + "slope": -0.238, + "direction": "down", + "confidence": 0.24 + }, + "slope30": { + "slope": -0.006, + "direction": "stable", + "confidence": 0 + }, + "avg30LastMonth": 59.7, + "avg30LastYear": null + }, + "ACTIVE_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FLIGHTS_CLIMBED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_RUNNING_DISTANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VO2_MAX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_FREE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MUSCLE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SKIN_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PULSE_WAVE_VELOCITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VASCULAR_AGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VISCERAL_FAT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_ENV": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_HEADPHONE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TIME_IN_DAYLIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESPIRATORY_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_MASS_INDEX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LEAN_BODY_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_HEART_RATE_AVERAGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_ASYMMETRY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_DOUBLE_SUPPORT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEP_LENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "CARDIO_RECOVERY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WRIST_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FALL_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SIX_MINUTE_WALK_DISTANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAIR_ASCENT_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAIR_DESCENT_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BREATHING_DISTURBANCES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "IRREGULAR_RHYTHM_NOTIFICATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HIGH_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LOW_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BREATHING_DISTURBANCE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RECOVERY_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRESS_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRAIN_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HRV_RMSSD": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "DAY_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WORKOUT_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_PERFORMANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_EFFICIENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_CONSISTENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_NEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ENERGY_EXPENDITURE_KJ": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AVERAGE_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MAX_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_DISTURBANCE_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ANS_CHARGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "CARDIO_LOAD": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE_DEVIATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESILIENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PHQ9_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GAD7_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GRIP_STRENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PAIN_NRS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_CIRCUMFERENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_TO_HEIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WHO5_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SCI_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "EXERCISE_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_HOURS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MINDFUL_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STATE_OF_MIND_VALENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AFIB_BURDEN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_CO2": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_HUMIDITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + } + }, + "bmi": 25.9, + "bpInTargetPct": 11, + "bpInTargetPct7d": 14, + "bpInTargetPct30d": 17, + "bpInTargetPctAllTime": 12, + "bpInTargetPctPriorMonth": 10, + "bpInTargetPctPriorYear": null, + "bpInTargetCount90": 90, + "bpInTargetSpanDays90": 90, + "glucoseByContext": { + "FASTING": { + "count": 20, + "latest": 92, + "min": 92, + "max": 98, + "mean": 94.85, + "median": 95, + "avg7": 95, + "avg30": 94.85, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.079, + "direction": "down", + "confidence": 0.06 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "POSTPRANDIAL": { + "count": 20, + "latest": 128, + "min": 128, + "max": 147, + "mean": 137.5, + "median": 137.5, + "avg7": 137, + "avg30": 137.5, + "slope7": { + "slope": -3, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.368, + "direction": "down", + "confidence": 0.14 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "UNSPECIFIED": { + "count": 20, + "latest": 105, + "min": 105, + "max": 108, + "mean": 106.5, + "median": 106.5, + "avg7": 106.29, + "avg30": 106.5, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.08 + }, + "slope30": { + "slope": -0.038, + "direction": "down", + "confidence": 0.04 + }, + "avg30LastMonth": null, + "avg30LastYear": null + } + }, + "glucoseClinical": { + "stillLearning": false, + "stillLearningReason": null, + "windowDays": 30, + "actualSpanDays": 19.197916666666668, + "readingCount": 60, + "meanMgdl": 112.95, + "distribution": { + "tir": 1, + "tbrLevel1": 0, + "tbrLevel2": 0, + "tarLevel1": 0, + "tarLevel2": 0, + "minutesEquivalent": { + "tir": 1440, + "tbrLevel1": 0, + "tbrLevel2": 0, + "tarLevel1": 0, + "tarLevel2": 0 + } + }, + "gmi": 6.011764, + "estimatedA1c": 5.562717770034843, + "variability": { + "sd": 18.50462608216329, + "cv": 16.383024419799284, + "unstable": false + }, + "advanced": { + "jIndex": 17.280318718401364, + "lbgi": 0.37857697435062837, + "hbgi": 0.48362299410937315 + }, + "isSpotEstimate": true + }, + "correlations": { + "bpCompliance": { + "kind": "bp-compliance", + "status": "insufficient", + "n": 24, + "reason": "not_significant", + "points": [ + { + "x": 100, + "y": 125 + }, + { + "x": 100, + "y": 133 + }, + { + "x": 100, + "y": 126 + }, + { + "x": 100, + "y": 134 + }, + { + "x": 100, + "y": 127 + }, + { + "x": 0, + "y": 135 + }, + { + "x": 100, + "y": 128 + }, + { + "x": 100, + "y": 136 + }, + { + "x": 100, + "y": 137 + }, + { + "x": 100, + "y": 130 + }, + { + "x": 100, + "y": 123 + }, + { + "x": 50, + "y": 131 + }, + { + "x": 100, + "y": 124 + }, + { + "x": 100, + "y": 132 + }, + { + "x": 100, + "y": 125 + }, + { + "x": 100, + "y": 133 + }, + { + "x": 100, + "y": 134 + }, + { + "x": 0, + "y": 127 + }, + { + "x": 100, + "y": 135 + }, + { + "x": 100, + "y": 128 + }, + { + "x": 100, + "y": 136 + }, + { + "x": 100, + "y": 129 + }, + { + "x": 100, + "y": 137 + }, + { + "x": 100, + "y": 130 + } + ] + }, + "moodPulse": { + "kind": "mood-pulse", + "status": "insufficient", + "n": 28, + "reason": "not_significant", + "points": [ + { + "x": 1, + "y": 58 + }, + { + "x": 4, + "y": 59 + }, + { + "x": 2, + "y": 60 + }, + { + "x": 5, + "y": 61 + }, + { + "x": 3, + "y": 58 + }, + { + "x": 1, + "y": 59 + }, + { + "x": 4, + "y": 60 + }, + { + "x": 2, + "y": 61 + }, + { + "x": 5, + "y": 58 + }, + { + "x": 3, + "y": 59 + }, + { + "x": 1, + "y": 60 + }, + { + "x": 4, + "y": 61 + }, + { + "x": 2, + "y": 58 + }, + { + "x": 5, + "y": 59 + }, + { + "x": 3, + "y": 60 + }, + { + "x": 1, + "y": 61 + }, + { + "x": 4, + "y": 58 + }, + { + "x": 2, + "y": 59 + }, + { + "x": 5, + "y": 60 + }, + { + "x": 3, + "y": 61 + }, + { + "x": 1, + "y": 58 + }, + { + "x": 4, + "y": 59 + }, + { + "x": 2, + "y": 60 + }, + { + "x": 5, + "y": 61 + }, + { + "x": 3, + "y": 58 + }, + { + "x": 1, + "y": 59 + }, + { + "x": 4, + "y": 60 + }, + { + "x": 2, + "y": 61 + } + ] + }, + "weightWeekday": { + "kind": "weight-weekday", + "status": "insufficient", + "n": 28, + "reason": "not_significant", + "points": [ + { + "x": 5, + "y": 83.1 + }, + { + "x": 6, + "y": 83 + }, + { + "x": 0, + "y": 83 + }, + { + "x": 1, + "y": 83 + }, + { + "x": 2, + "y": 82.9 + }, + { + "x": 3, + "y": 82.9 + }, + { + "x": 4, + "y": 82.8 + }, + { + "x": 5, + "y": 82.8 + }, + { + "x": 6, + "y": 82.8 + }, + { + "x": 0, + "y": 82.7 + }, + { + "x": 1, + "y": 82.7 + }, + { + "x": 2, + "y": 82.6 + }, + { + "x": 3, + "y": 82.6 + }, + { + "x": 4, + "y": 82.6 + }, + { + "x": 5, + "y": 82.5 + }, + { + "x": 6, + "y": 82.5 + }, + { + "x": 0, + "y": 82.4 + }, + { + "x": 1, + "y": 82.4 + }, + { + "x": 2, + "y": 82.4 + }, + { + "x": 3, + "y": 82.3 + }, + { + "x": 4, + "y": 82.3 + }, + { + "x": 5, + "y": 82.2 + }, + { + "x": 6, + "y": 82.2 + }, + { + "x": 0, + "y": 82.2 + }, + { + "x": 1, + "y": 82.1 + }, + { + "x": 2, + "y": 82.1 + }, + { + "x": 3, + "y": 82 + }, + { + "x": 4, + "y": 82 + } + ] + }, + "degraded": false, + "windowDays": 28, + "path": "rollup" + }, + "healthScore": { + "composite": { + "status": "ok", + "value": { + "score": 86, + "band": "green", + "bandSetter": null, + "composition": [ + "BLOOD_PRESSURE", + "GLYCAEMIA", + "ACTIVITY", + "SLEEP" + ], + "configured": false, + "scoreBasis": { + "domains": 3, + "recommended": 3, + "tier": "full", + "physiological": true + }, + "noiseFloor": 2, + "scoreVersion": 4 + }, + "coverage": { + "requiredInputs": 3, + "presentInputs": 3, + "historyDays": 20, + "missing": [ + "adiposity", + "wellbeing" + ] + }, + "confidence": { + "score": 71, + "band": "medium" + }, + "provenance": { + "inputs": [ + "BLOOD_PRESSURE", + "GLYCAEMIA", + "ACTIVITY", + "SLEEP" + ], + "source": "live", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + } + }, + "pillars": [ + { + "id": "BLOOD_PRESSURE", + "domain": "cardiometabolic", + "result": { + "status": "ok", + "value": { + "score": 73, + "scoreBasis": { + "axis": "diastolic", + "relation": "above_ceiling", + "offsetMmHg": 3, + "boundaryMmHg": 79 + }, + "observed": { + "value": 130, + "unit": "mmHg", + "label": "130/82 mmHg", + "asOf": "anchor+0d+00:00:00", + "sources": [ + "MANUAL" + ] + }, + "reference": { + "kind": "clinical-threshold", + "low": 120, + "high": 129, + "label": "120–129/70–79 mmHg", + "source": "ESH 2023" + }, + "noiseFloor": 1, + "deltaEligible": true, + "deltaIdentity": "graded_bp_pair_series" + }, + "coverage": { + "requiredInputs": 12, + "presentInputs": 12, + "historyDays": 90, + "missing": [] + }, + "confidence": { + "score": 100, + "band": "high" + }, + "provenance": { + "inputs": [ + "BLOOD_PRESSURE" + ], + "source": "DAY", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + } + } + }, + { + "id": "GLYCAEMIA", + "domain": "cardiometabolic", + "result": { + "status": "ok", + "value": { + "score": 100, + "observed": { + "value": 95, + "unit": "mg/dL", + "label": "Fasting median 95 mg/dL", + "asOf": "anchor+0d+06:15:00", + "sources": [ + "MANUAL" + ] + }, + "reference": { + "kind": "clinical-threshold", + "low": 70, + "high": 99, + "label": "70–99 mg/dL fasting", + "source": "ADA 2025; Selvin 2010" + }, + "noiseFloor": 1, + "deltaEligible": true, + "deltaIdentity": "fasting_glucose" + }, + "coverage": { + "requiredInputs": 8, + "presentInputs": 8, + "historyDays": 20, + "missing": [] + }, + "confidence": { + "score": 22, + "band": "draft" + }, + "provenance": { + "inputs": [ + "GLYCAEMIA" + ], + "source": "live", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + } + } + }, + { + "id": "ACTIVITY", + "domain": "activity", + "result": { + "status": "ok", + "value": { + "score": 78, + "observed": { + "value": 7837, + "unit": "steps/day", + "label": "7,837 steps/day", + "asOf": "anchor-1d+00:00:00", + "sources": [ + "APPLE_HEALTH" + ] + }, + "reference": { + "kind": "guideline-band", + "low": 0, + "high": 10000, + "label": "benefit plateaus by 10,000 steps/day", + "source": "Paluch 2022" + }, + "noiseFloor": 5, + "deltaEligible": true, + "deltaIdentity": "ACTIVITY_STEPS" + }, + "coverage": { + "requiredInputs": 21, + "presentInputs": 21, + "historyDays": 28, + "missing": [] + }, + "confidence": { + "score": 100, + "band": "high" + }, + "provenance": { + "inputs": [ + "ACTIVITY" + ], + "source": "live", + "windowDays": 28, + "computedAt": "anchor+0d+12:00:00" + } + } + }, + { + "id": "SLEEP", + "domain": "sleep", + "result": { + "status": "ok", + "value": { + "score": 92, + "observed": { + "value": 388, + "unit": "minutes/night", + "label": "388 min/night + regularity", + "asOf": "anchor+0d+00:00:00", + "sources": [ + "APPLE_HEALTH" + ] + }, + "reference": { + "kind": "guideline-band", + "low": 420, + "high": null, + "label": "at least 420 min/night; long sleep is not penalised", + "source": "NSF 2015; Ai 2021; Daghlas 2019; Windred 2024" + }, + "noiseFloor": 2, + "deltaEligible": true, + "deltaIdentity": "sleep_duration_rhythm" + }, + "coverage": { + "requiredInputs": 14, + "presentInputs": 14, + "historyDays": 21, + "missing": [] + }, + "confidence": { + "score": 75, + "band": "high" + }, + "provenance": { + "inputs": [ + "SLEEP" + ], + "source": "live", + "windowDays": 28, + "computedAt": "anchor+0d+12:00:00" + } + } + }, + { + "id": "ADIPOSITY", + "domain": "adiposity", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 1, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "waist measurement" + ] + }, + "provenance": { + "inputs": [ + "ADIPOSITY" + ], + "source": "live", + "windowDays": 180, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + }, + { + "id": "WELLBEING", + "domain": "wellbeing", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 1, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "completed WHO-5, PHQ-9 or GAD-7" + ] + }, + "provenance": { + "inputs": [ + "WELLBEING" + ], + "source": "live", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + }, + { + "id": "LIPIDS", + "domain": "cardiometabolic", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 1, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "lipid panel with lab reference ranges" + ] + }, + "provenance": { + "inputs": [ + "LIPIDS" + ], + "source": "live", + "windowDays": 365, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + } + ], + "delta": null, + "deltaReason": "first_eligibility_window", + "scoreVersion": 4, + "weightGoal": { + "status": "insufficient", + "coverage": { + "requiredInputs": 2, + "presentInputs": 1, + "historyDays": 1, + "missing": [ + "personal weight target" + ] + }, + "provenance": { + "inputs": [ + "WEIGHT", + "personal weight target" + ], + "source": "live", + "windowDays": 60, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "no_personal_goal" + }, + "algorithmNotice": { + "itemKey": "health_score_algorithm:4", + "dismissed": false + }, + "compositionNotice": null, + "restMode": { + "active": true, + "since": "anchor-3d+12:00:00", + "episodeCount": 1 + } + }, + "sleepStages": { + "windowDays": 30, + "nights": 21, + "totalMinutes": 8362, + "stages": { + "CORE": 4600, + "DEEP": 1575, + "REM": 1935, + "AWAKE": 252 + }, + "perNight": [ + { + "dayKey": "day-20", + "stages": { + "CORE": 200, + "DEEP": 80, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-19", + "stages": { + "CORE": 240, + "DEEP": 75, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-18", + "stages": { + "CORE": 230, + "DEEP": 70, + "REM": 95, + "AWAKE": 12 + } + }, + { + "dayKey": "day-17", + "stages": { + "CORE": 220, + "DEEP": 80, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day-16", + "stages": { + "CORE": 210, + "DEEP": 75, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-15", + "stages": { + "CORE": 200, + "DEEP": 70, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-14", + "stages": { + "CORE": 240, + "DEEP": 80, + "REM": 95, + "AWAKE": 12 + } + }, + { + "dayKey": "day-13", + "stages": { + "CORE": 230, + "DEEP": 75, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day-12", + "stages": { + "CORE": 220, + "DEEP": 70, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-11", + "stages": { + "CORE": 210, + "DEEP": 80, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-10", + "stages": { + "CORE": 200, + "DEEP": 75, + "REM": 95, + "AWAKE": 12 + } + }, + { + "dayKey": "day-9", + "stages": { + "CORE": 240, + "DEEP": 70, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day-8", + "stages": { + "CORE": 230, + "DEEP": 80, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-7", + "stages": { + "CORE": 220, + "DEEP": 75, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-6", + "stages": { + "CORE": 210, + "DEEP": 70, + "REM": 95, + "AWAKE": 12 + } + }, + { + "dayKey": "day-5", + "stages": { + "CORE": 200, + "DEEP": 80, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day-4", + "stages": { + "CORE": 240, + "DEEP": 75, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-3", + "stages": { + "CORE": 230, + "DEEP": 70, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-2", + "stages": { + "CORE": 220, + "DEEP": 80, + "REM": 95, + "AWAKE": 12 + }, + "napMinutes": 35, + "napCount": 1 + }, + { + "dayKey": "day-1", + "stages": { + "CORE": 210, + "DEEP": 75, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day+0", + "stages": { + "CORE": 200, + "DEEP": 70, + "REM": 85, + "AWAKE": 12 + } + } + ] + }, + "sleepSourceDiscrepancy": null, + "lastSeenByType": { + "WEIGHT": { + "lastSeenAt": "anchor+0d+06:30:00", + "daysAgo": 0 + }, + "BLOOD_PRESSURE_SYS": { + "lastSeenAt": "anchor+0d+07:00:00", + "daysAgo": 0 + }, + "BLOOD_PRESSURE_DIA": { + "lastSeenAt": "anchor+0d+07:00:00", + "daysAgo": 0 + }, + "PULSE": { + "lastSeenAt": "anchor+0d+07:01:00", + "daysAgo": 0 + }, + "BODY_FAT": { + "lastSeenAt": "anchor+0d+06:31:00", + "daysAgo": 0 + }, + "SLEEP_DURATION": { + "lastSeenAt": "anchor+0d+04:15:00", + "daysAgo": 0 + }, + "ACTIVITY_STEPS": { + "lastSeenAt": "anchor+0d+09:00:00", + "daysAgo": 0 + }, + "BLOOD_GLUCOSE": { + "lastSeenAt": "anchor+0d+11:00:00", + "daysAgo": 0 + }, + "TOTAL_BODY_WATER": null, + "BONE_MASS": null, + "OXYGEN_SATURATION": { + "lastSeenAt": "anchor+0d+04:40:00", + "daysAgo": 0 + }, + "HEART_RATE_VARIABILITY": { + "lastSeenAt": "anchor+0d+04:30:00", + "daysAgo": 0 + }, + "RESTING_HEART_RATE": { + "lastSeenAt": "anchor+0d+05:00:00", + "daysAgo": 0 + }, + "ACTIVE_ENERGY_BURNED": null, + "FLIGHTS_CLIMBED": null, + "WALKING_RUNNING_DISTANCE": null, + "VO2_MAX": null, + "BODY_TEMPERATURE": null, + "FAT_FREE_MASS": null, + "FAT_MASS": null, + "MUSCLE_MASS": null, + "SKIN_TEMPERATURE": null, + "PULSE_WAVE_VELOCITY": null, + "VASCULAR_AGE": null, + "VISCERAL_FAT": null, + "AUDIO_EXPOSURE_ENV": null, + "AUDIO_EXPOSURE_HEADPHONE": null, + "TIME_IN_DAYLIGHT": null, + "WALKING_STEADINESS": null, + "AUDIO_EXPOSURE_EVENT": null, + "RESPIRATORY_RATE": null, + "BODY_MASS_INDEX": null, + "LEAN_BODY_MASS": null, + "WALKING_HEART_RATE_AVERAGE": null, + "WALKING_ASYMMETRY": null, + "WALKING_DOUBLE_SUPPORT": null, + "WALKING_STEP_LENGTH": null, + "WALKING_SPEED": null, + "CARDIO_RECOVERY": null, + "WRIST_TEMPERATURE": null, + "FALL_COUNT": null, + "SIX_MINUTE_WALK_DISTANCE": null, + "STAIR_ASCENT_SPEED": null, + "STAIR_DESCENT_SPEED": null, + "BREATHING_DISTURBANCES": null, + "IRREGULAR_RHYTHM_NOTIFICATION": null, + "HIGH_HEART_RATE_EVENT": null, + "LOW_HEART_RATE_EVENT": null, + "WALKING_STEADINESS_EVENT": null, + "BREATHING_DISTURBANCE_EVENT": null, + "RECOVERY_SCORE": null, + "STRESS_SCORE": null, + "STRAIN_SCORE": null, + "HRV_RMSSD": null, + "DAY_STRAIN": null, + "WORKOUT_STRAIN": null, + "SLEEP_PERFORMANCE": null, + "SLEEP_EFFICIENCY": null, + "SLEEP_CONSISTENCY": null, + "SLEEP_NEED": null, + "ENERGY_EXPENDITURE_KJ": null, + "AVERAGE_HEART_RATE": null, + "MAX_HEART_RATE": null, + "SLEEP_DISTURBANCE_COUNT": null, + "ANS_CHARGE": null, + "CARDIO_LOAD": null, + "SLEEP_SCORE": null, + "BODY_TEMPERATURE_DEVIATION": null, + "RESILIENCE": null, + "PHQ9_SCORE": null, + "GAD7_SCORE": null, + "GRIP_STRENGTH": null, + "PAIN_NRS": null, + "WAIST_CIRCUMFERENCE": null, + "WAIST_TO_HEIGHT": null, + "WHO5_SCORE": null, + "SCI_SCORE": null, + "EXERCISE_MINUTES": null, + "STAND_MINUTES": null, + "STAND_HOURS": null, + "MINDFUL_MINUTES": null, + "STATE_OF_MIND_VALENCE": null, + "AFIB_BURDEN": null, + "INDOOR_CO2": null, + "INDOOR_TEMPERATURE": null, + "INDOOR_HUMIDITY": null + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "score.record.write" + }, + "meta": { + "cache.summaries_slice.outcome": "hit", + "analytics": { + "bp_in_target": { + "path": "rollup", + "tz_guard": "near-utc" + } + }, + "correlations": { + "bpCompliance": "insufficient", + "moodPulse": "insufficient", + "weightWeekday": "insufficient", + "path": "rollup", + "window_days": 28, + "degraded": false, + "tz_guard": "near-utc" + }, + "dayKey": "day+0", + "scoreVersion": 4, + "pillars": 4, + "cache.analytics.outcome": "miss", + "cache.analytics.key_hash": 364623505 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserRollupsFresh", + "args": [ + "" + ] + } + ], + "stored": { + "healthScores": [ + { + "dayKey": "day+0", + "timezone": "UTC", + "composite": 86, + "band": "green", + "scoreVersion": 4, + "composition": [ + "BLOOD_PRESSURE", + "GLYCAEMIA", + "ACTIVITY", + "SLEEP" + ], + "pillarScores": { + "SLEEP": 92, + "ACTIVITY": 78, + "GLYCAEMIA": 100, + "BLOOD_PRESSURE": 73 + }, + "inputFingerprint": "5653cd8c051404182d6c6f813a48b1c2ebc3abe1116901466ab7cc1ab70cb276", + "computedAt": "anchor+0d+12:00:00" + } + ], + "patterns": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/analytics.05-default-warm.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.05-default-warm.json.snap new file mode 100644 index 000000000..528935f1e --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/analytics.05-default-warm.json.snap @@ -0,0 +1,2482 @@ +{ + "status": 200, + "cacheControl": "private, max-age=0, must-revalidate", + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 60, + "latest": 82, + "min": 82, + "max": 84.4, + "mean": 83.18, + "median": 83.2, + "avg7": 82.11, + "avg30": 82.58, + "slope7": { + "slope": -0.042, + "direction": "down", + "confidence": 0.93 + }, + "slope30": { + "slope": -0.04, + "direction": "down", + "confidence": 0.99 + }, + "avg30LastMonth": 83.78, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 91, + "latest": 123, + "min": 123, + "max": 137, + "mean": 129.92, + "median": 130, + "avg7": 131.14, + "avg30": 130, + "slope7": { + "slope": -0.393, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": 0.071, + "direction": "up", + "confidence": 0.02 + }, + "avg30LastMonth": 130, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 91, + "latest": 78, + "min": 78, + "max": 86, + "mean": 81.96, + "median": 82, + "avg7": 81.43, + "avg30": 81.8, + "slope7": { + "slope": -0.714, + "direction": "down", + "confidence": 0.36 + }, + "slope30": { + "slope": -0.017, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": 82.1, + "avg30LastYear": null + }, + "PULSE": { + "count": 60, + "latest": 64, + "min": 64, + "max": 69, + "mean": 66.5, + "median": 66.5, + "avg7": 66.14, + "avg30": 66.5, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": -0.02, + "direction": "down", + "confidence": 0.01 + }, + "avg30LastMonth": 66.5, + "avg30LastYear": null + }, + "BODY_FAT": { + "count": 9, + "latest": 24, + "min": 24, + "max": 25.1, + "mean": 24.56, + "median": 24.6, + "avg7": 24, + "avg30": 24.28, + "slope7": { + "slope": -0.014, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.021, + "direction": "down", + "confidence": 0.99 + }, + "avg30LastMonth": 24.9, + "avg30LastYear": null + }, + "SLEEP_DURATION": { + "count": 21, + "latest": 355, + "min": 355, + "max": 430, + "mean": 387.86, + "median": 390, + "avg7": 386.43, + "avg30": 387.86, + "slope7": { + "slope": -0.1, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.03, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVITY_STEPS": { + "count": 30, + "latest": 6000, + "min": 6000, + "max": 9833, + "mean": 7808.5, + "median": 7781.5, + "avg7": 7119, + "avg30": 7808.5, + "slope7": { + "slope": -373, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -30.397, + "direction": "down", + "confidence": 0.06 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BLOOD_GLUCOSE": { + "count": 60, + "latest": 128, + "min": 92, + "max": 147, + "mean": 112.95, + "median": 106.5, + "avg7": 112.76, + "avg30": 112.95, + "slope7": { + "slope": -0.424, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.123, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TOTAL_BODY_WATER": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BONE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "OXYGEN_SATURATION": { + "count": 10, + "latest": 96, + "min": 96, + "max": 98, + "mean": 96.9, + "median": 97, + "avg7": 96.86, + "avg30": 96.9, + "slope7": { + "slope": -0.036, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.018, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HEART_RATE_VARIABILITY": { + "count": 30, + "latest": 45, + "min": 45, + "max": 52, + "mean": 48.3, + "median": 48, + "avg7": 48, + "avg30": 48.3, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.032, + "direction": "down", + "confidence": 0.02 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESTING_HEART_RATE": { + "count": 40, + "latest": 58, + "min": 58, + "max": 61, + "mean": 59.5, + "median": 59.5, + "avg7": 59.29, + "avg30": 59.43, + "slope7": { + "slope": -0.238, + "direction": "down", + "confidence": 0.24 + }, + "slope30": { + "slope": -0.006, + "direction": "stable", + "confidence": 0 + }, + "avg30LastMonth": 59.7, + "avg30LastYear": null + }, + "ACTIVE_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FLIGHTS_CLIMBED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_RUNNING_DISTANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VO2_MAX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_FREE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MUSCLE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SKIN_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PULSE_WAVE_VELOCITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VASCULAR_AGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VISCERAL_FAT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_ENV": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_HEADPHONE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TIME_IN_DAYLIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESPIRATORY_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_MASS_INDEX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LEAN_BODY_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_HEART_RATE_AVERAGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_ASYMMETRY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_DOUBLE_SUPPORT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEP_LENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "CARDIO_RECOVERY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WRIST_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FALL_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SIX_MINUTE_WALK_DISTANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAIR_ASCENT_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAIR_DESCENT_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BREATHING_DISTURBANCES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "IRREGULAR_RHYTHM_NOTIFICATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HIGH_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LOW_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BREATHING_DISTURBANCE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RECOVERY_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRESS_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRAIN_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HRV_RMSSD": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "DAY_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WORKOUT_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_PERFORMANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_EFFICIENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_CONSISTENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_NEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ENERGY_EXPENDITURE_KJ": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AVERAGE_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MAX_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_DISTURBANCE_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ANS_CHARGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "CARDIO_LOAD": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE_DEVIATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESILIENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PHQ9_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GAD7_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GRIP_STRENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PAIN_NRS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_CIRCUMFERENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_TO_HEIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WHO5_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SCI_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "EXERCISE_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_HOURS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MINDFUL_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STATE_OF_MIND_VALENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AFIB_BURDEN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_CO2": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_HUMIDITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + } + }, + "bmi": 25.9, + "bpInTargetPct": 11, + "bpInTargetPct7d": 14, + "bpInTargetPct30d": 17, + "bpInTargetPctAllTime": 12, + "bpInTargetPctPriorMonth": 10, + "bpInTargetPctPriorYear": null, + "bpInTargetCount90": 90, + "bpInTargetSpanDays90": 90, + "glucoseByContext": { + "FASTING": { + "count": 20, + "latest": 92, + "min": 92, + "max": 98, + "mean": 94.85, + "median": 95, + "avg7": 95, + "avg30": 94.85, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.079, + "direction": "down", + "confidence": 0.06 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "POSTPRANDIAL": { + "count": 20, + "latest": 128, + "min": 128, + "max": 147, + "mean": 137.5, + "median": 137.5, + "avg7": 137, + "avg30": 137.5, + "slope7": { + "slope": -3, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.368, + "direction": "down", + "confidence": 0.14 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "UNSPECIFIED": { + "count": 20, + "latest": 105, + "min": 105, + "max": 108, + "mean": 106.5, + "median": 106.5, + "avg7": 106.29, + "avg30": 106.5, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.08 + }, + "slope30": { + "slope": -0.038, + "direction": "down", + "confidence": 0.04 + }, + "avg30LastMonth": null, + "avg30LastYear": null + } + }, + "glucoseClinical": { + "stillLearning": false, + "stillLearningReason": null, + "windowDays": 30, + "actualSpanDays": 19.197916666666668, + "readingCount": 60, + "meanMgdl": 112.95, + "distribution": { + "tir": 1, + "tbrLevel1": 0, + "tbrLevel2": 0, + "tarLevel1": 0, + "tarLevel2": 0, + "minutesEquivalent": { + "tir": 1440, + "tbrLevel1": 0, + "tbrLevel2": 0, + "tarLevel1": 0, + "tarLevel2": 0 + } + }, + "gmi": 6.011764, + "estimatedA1c": 5.562717770034843, + "variability": { + "sd": 18.50462608216329, + "cv": 16.383024419799284, + "unstable": false + }, + "advanced": { + "jIndex": 17.280318718401364, + "lbgi": 0.37857697435062837, + "hbgi": 0.48362299410937315 + }, + "isSpotEstimate": true + }, + "correlations": { + "bpCompliance": { + "kind": "bp-compliance", + "status": "insufficient", + "n": 24, + "reason": "not_significant", + "points": [ + { + "x": 100, + "y": 125 + }, + { + "x": 100, + "y": 133 + }, + { + "x": 100, + "y": 126 + }, + { + "x": 100, + "y": 134 + }, + { + "x": 100, + "y": 127 + }, + { + "x": 0, + "y": 135 + }, + { + "x": 100, + "y": 128 + }, + { + "x": 100, + "y": 136 + }, + { + "x": 100, + "y": 137 + }, + { + "x": 100, + "y": 130 + }, + { + "x": 100, + "y": 123 + }, + { + "x": 50, + "y": 131 + }, + { + "x": 100, + "y": 124 + }, + { + "x": 100, + "y": 132 + }, + { + "x": 100, + "y": 125 + }, + { + "x": 100, + "y": 133 + }, + { + "x": 100, + "y": 134 + }, + { + "x": 0, + "y": 127 + }, + { + "x": 100, + "y": 135 + }, + { + "x": 100, + "y": 128 + }, + { + "x": 100, + "y": 136 + }, + { + "x": 100, + "y": 129 + }, + { + "x": 100, + "y": 137 + }, + { + "x": 100, + "y": 130 + } + ] + }, + "moodPulse": { + "kind": "mood-pulse", + "status": "insufficient", + "n": 28, + "reason": "not_significant", + "points": [ + { + "x": 1, + "y": 58 + }, + { + "x": 4, + "y": 59 + }, + { + "x": 2, + "y": 60 + }, + { + "x": 5, + "y": 61 + }, + { + "x": 3, + "y": 58 + }, + { + "x": 1, + "y": 59 + }, + { + "x": 4, + "y": 60 + }, + { + "x": 2, + "y": 61 + }, + { + "x": 5, + "y": 58 + }, + { + "x": 3, + "y": 59 + }, + { + "x": 1, + "y": 60 + }, + { + "x": 4, + "y": 61 + }, + { + "x": 2, + "y": 58 + }, + { + "x": 5, + "y": 59 + }, + { + "x": 3, + "y": 60 + }, + { + "x": 1, + "y": 61 + }, + { + "x": 4, + "y": 58 + }, + { + "x": 2, + "y": 59 + }, + { + "x": 5, + "y": 60 + }, + { + "x": 3, + "y": 61 + }, + { + "x": 1, + "y": 58 + }, + { + "x": 4, + "y": 59 + }, + { + "x": 2, + "y": 60 + }, + { + "x": 5, + "y": 61 + }, + { + "x": 3, + "y": 58 + }, + { + "x": 1, + "y": 59 + }, + { + "x": 4, + "y": 60 + }, + { + "x": 2, + "y": 61 + } + ] + }, + "weightWeekday": { + "kind": "weight-weekday", + "status": "insufficient", + "n": 28, + "reason": "not_significant", + "points": [ + { + "x": 5, + "y": 83.1 + }, + { + "x": 6, + "y": 83 + }, + { + "x": 0, + "y": 83 + }, + { + "x": 1, + "y": 83 + }, + { + "x": 2, + "y": 82.9 + }, + { + "x": 3, + "y": 82.9 + }, + { + "x": 4, + "y": 82.8 + }, + { + "x": 5, + "y": 82.8 + }, + { + "x": 6, + "y": 82.8 + }, + { + "x": 0, + "y": 82.7 + }, + { + "x": 1, + "y": 82.7 + }, + { + "x": 2, + "y": 82.6 + }, + { + "x": 3, + "y": 82.6 + }, + { + "x": 4, + "y": 82.6 + }, + { + "x": 5, + "y": 82.5 + }, + { + "x": 6, + "y": 82.5 + }, + { + "x": 0, + "y": 82.4 + }, + { + "x": 1, + "y": 82.4 + }, + { + "x": 2, + "y": 82.4 + }, + { + "x": 3, + "y": 82.3 + }, + { + "x": 4, + "y": 82.3 + }, + { + "x": 5, + "y": 82.2 + }, + { + "x": 6, + "y": 82.2 + }, + { + "x": 0, + "y": 82.2 + }, + { + "x": 1, + "y": 82.1 + }, + { + "x": 2, + "y": 82.1 + }, + { + "x": 3, + "y": 82 + }, + { + "x": 4, + "y": 82 + } + ] + }, + "degraded": false, + "windowDays": 28, + "path": "rollup" + }, + "healthScore": { + "composite": { + "status": "ok", + "value": { + "score": 86, + "band": "green", + "bandSetter": null, + "composition": [ + "BLOOD_PRESSURE", + "GLYCAEMIA", + "ACTIVITY", + "SLEEP" + ], + "configured": false, + "scoreBasis": { + "domains": 3, + "recommended": 3, + "tier": "full", + "physiological": true + }, + "noiseFloor": 2, + "scoreVersion": 4 + }, + "coverage": { + "requiredInputs": 3, + "presentInputs": 3, + "historyDays": 20, + "missing": [ + "adiposity", + "wellbeing" + ] + }, + "confidence": { + "score": 71, + "band": "medium" + }, + "provenance": { + "inputs": [ + "BLOOD_PRESSURE", + "GLYCAEMIA", + "ACTIVITY", + "SLEEP" + ], + "source": "live", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + } + }, + "pillars": [ + { + "id": "BLOOD_PRESSURE", + "domain": "cardiometabolic", + "result": { + "status": "ok", + "value": { + "score": 73, + "scoreBasis": { + "axis": "diastolic", + "relation": "above_ceiling", + "offsetMmHg": 3, + "boundaryMmHg": 79 + }, + "observed": { + "value": 130, + "unit": "mmHg", + "label": "130/82 mmHg", + "asOf": "anchor+0d+00:00:00", + "sources": [ + "MANUAL" + ] + }, + "reference": { + "kind": "clinical-threshold", + "low": 120, + "high": 129, + "label": "120–129/70–79 mmHg", + "source": "ESH 2023" + }, + "noiseFloor": 1, + "deltaEligible": true, + "deltaIdentity": "graded_bp_pair_series" + }, + "coverage": { + "requiredInputs": 12, + "presentInputs": 12, + "historyDays": 90, + "missing": [] + }, + "confidence": { + "score": 100, + "band": "high" + }, + "provenance": { + "inputs": [ + "BLOOD_PRESSURE" + ], + "source": "DAY", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + } + } + }, + { + "id": "GLYCAEMIA", + "domain": "cardiometabolic", + "result": { + "status": "ok", + "value": { + "score": 100, + "observed": { + "value": 95, + "unit": "mg/dL", + "label": "Fasting median 95 mg/dL", + "asOf": "anchor+0d+06:15:00", + "sources": [ + "MANUAL" + ] + }, + "reference": { + "kind": "clinical-threshold", + "low": 70, + "high": 99, + "label": "70–99 mg/dL fasting", + "source": "ADA 2025; Selvin 2010" + }, + "noiseFloor": 1, + "deltaEligible": true, + "deltaIdentity": "fasting_glucose" + }, + "coverage": { + "requiredInputs": 8, + "presentInputs": 8, + "historyDays": 20, + "missing": [] + }, + "confidence": { + "score": 22, + "band": "draft" + }, + "provenance": { + "inputs": [ + "GLYCAEMIA" + ], + "source": "live", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + } + } + }, + { + "id": "ACTIVITY", + "domain": "activity", + "result": { + "status": "ok", + "value": { + "score": 78, + "observed": { + "value": 7837, + "unit": "steps/day", + "label": "7,837 steps/day", + "asOf": "anchor-1d+00:00:00", + "sources": [ + "APPLE_HEALTH" + ] + }, + "reference": { + "kind": "guideline-band", + "low": 0, + "high": 10000, + "label": "benefit plateaus by 10,000 steps/day", + "source": "Paluch 2022" + }, + "noiseFloor": 5, + "deltaEligible": true, + "deltaIdentity": "ACTIVITY_STEPS" + }, + "coverage": { + "requiredInputs": 21, + "presentInputs": 21, + "historyDays": 28, + "missing": [] + }, + "confidence": { + "score": 100, + "band": "high" + }, + "provenance": { + "inputs": [ + "ACTIVITY" + ], + "source": "live", + "windowDays": 28, + "computedAt": "anchor+0d+12:00:00" + } + } + }, + { + "id": "SLEEP", + "domain": "sleep", + "result": { + "status": "ok", + "value": { + "score": 92, + "observed": { + "value": 388, + "unit": "minutes/night", + "label": "388 min/night + regularity", + "asOf": "anchor+0d+00:00:00", + "sources": [ + "APPLE_HEALTH" + ] + }, + "reference": { + "kind": "guideline-band", + "low": 420, + "high": null, + "label": "at least 420 min/night; long sleep is not penalised", + "source": "NSF 2015; Ai 2021; Daghlas 2019; Windred 2024" + }, + "noiseFloor": 2, + "deltaEligible": true, + "deltaIdentity": "sleep_duration_rhythm" + }, + "coverage": { + "requiredInputs": 14, + "presentInputs": 14, + "historyDays": 21, + "missing": [] + }, + "confidence": { + "score": 75, + "band": "high" + }, + "provenance": { + "inputs": [ + "SLEEP" + ], + "source": "live", + "windowDays": 28, + "computedAt": "anchor+0d+12:00:00" + } + } + }, + { + "id": "ADIPOSITY", + "domain": "adiposity", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 1, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "waist measurement" + ] + }, + "provenance": { + "inputs": [ + "ADIPOSITY" + ], + "source": "live", + "windowDays": 180, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + }, + { + "id": "WELLBEING", + "domain": "wellbeing", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 1, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "completed WHO-5, PHQ-9 or GAD-7" + ] + }, + "provenance": { + "inputs": [ + "WELLBEING" + ], + "source": "live", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + }, + { + "id": "LIPIDS", + "domain": "cardiometabolic", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 1, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "lipid panel with lab reference ranges" + ] + }, + "provenance": { + "inputs": [ + "LIPIDS" + ], + "source": "live", + "windowDays": 365, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + } + ], + "delta": null, + "deltaReason": "first_eligibility_window", + "scoreVersion": 4, + "weightGoal": { + "status": "insufficient", + "coverage": { + "requiredInputs": 2, + "presentInputs": 1, + "historyDays": 1, + "missing": [ + "personal weight target" + ] + }, + "provenance": { + "inputs": [ + "WEIGHT", + "personal weight target" + ], + "source": "live", + "windowDays": 60, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "no_personal_goal" + }, + "algorithmNotice": { + "itemKey": "health_score_algorithm:4", + "dismissed": false + }, + "compositionNotice": null, + "restMode": { + "active": true, + "since": "anchor-3d+12:00:00", + "episodeCount": 1 + } + }, + "sleepStages": { + "windowDays": 30, + "nights": 21, + "totalMinutes": 8362, + "stages": { + "CORE": 4600, + "DEEP": 1575, + "REM": 1935, + "AWAKE": 252 + }, + "perNight": [ + { + "dayKey": "day-20", + "stages": { + "CORE": 200, + "DEEP": 80, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-19", + "stages": { + "CORE": 240, + "DEEP": 75, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-18", + "stages": { + "CORE": 230, + "DEEP": 70, + "REM": 95, + "AWAKE": 12 + } + }, + { + "dayKey": "day-17", + "stages": { + "CORE": 220, + "DEEP": 80, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day-16", + "stages": { + "CORE": 210, + "DEEP": 75, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-15", + "stages": { + "CORE": 200, + "DEEP": 70, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-14", + "stages": { + "CORE": 240, + "DEEP": 80, + "REM": 95, + "AWAKE": 12 + } + }, + { + "dayKey": "day-13", + "stages": { + "CORE": 230, + "DEEP": 75, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day-12", + "stages": { + "CORE": 220, + "DEEP": 70, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-11", + "stages": { + "CORE": 210, + "DEEP": 80, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-10", + "stages": { + "CORE": 200, + "DEEP": 75, + "REM": 95, + "AWAKE": 12 + } + }, + { + "dayKey": "day-9", + "stages": { + "CORE": 240, + "DEEP": 70, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day-8", + "stages": { + "CORE": 230, + "DEEP": 80, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-7", + "stages": { + "CORE": 220, + "DEEP": 75, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-6", + "stages": { + "CORE": 210, + "DEEP": 70, + "REM": 95, + "AWAKE": 12 + } + }, + { + "dayKey": "day-5", + "stages": { + "CORE": 200, + "DEEP": 80, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day-4", + "stages": { + "CORE": 240, + "DEEP": 75, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-3", + "stages": { + "CORE": 230, + "DEEP": 70, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-2", + "stages": { + "CORE": 220, + "DEEP": 80, + "REM": 95, + "AWAKE": 12 + }, + "napMinutes": 35, + "napCount": 1 + }, + { + "dayKey": "day-1", + "stages": { + "CORE": 210, + "DEEP": 75, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day+0", + "stages": { + "CORE": 200, + "DEEP": 70, + "REM": 85, + "AWAKE": 12 + } + } + ] + }, + "sleepSourceDiscrepancy": null, + "lastSeenByType": { + "WEIGHT": { + "lastSeenAt": "anchor+0d+06:30:00", + "daysAgo": 0 + }, + "BLOOD_PRESSURE_SYS": { + "lastSeenAt": "anchor+0d+07:00:00", + "daysAgo": 0 + }, + "BLOOD_PRESSURE_DIA": { + "lastSeenAt": "anchor+0d+07:00:00", + "daysAgo": 0 + }, + "PULSE": { + "lastSeenAt": "anchor+0d+07:01:00", + "daysAgo": 0 + }, + "BODY_FAT": { + "lastSeenAt": "anchor+0d+06:31:00", + "daysAgo": 0 + }, + "SLEEP_DURATION": { + "lastSeenAt": "anchor+0d+04:15:00", + "daysAgo": 0 + }, + "ACTIVITY_STEPS": { + "lastSeenAt": "anchor+0d+09:00:00", + "daysAgo": 0 + }, + "BLOOD_GLUCOSE": { + "lastSeenAt": "anchor+0d+11:00:00", + "daysAgo": 0 + }, + "TOTAL_BODY_WATER": null, + "BONE_MASS": null, + "OXYGEN_SATURATION": { + "lastSeenAt": "anchor+0d+04:40:00", + "daysAgo": 0 + }, + "HEART_RATE_VARIABILITY": { + "lastSeenAt": "anchor+0d+04:30:00", + "daysAgo": 0 + }, + "RESTING_HEART_RATE": { + "lastSeenAt": "anchor+0d+05:00:00", + "daysAgo": 0 + }, + "ACTIVE_ENERGY_BURNED": null, + "FLIGHTS_CLIMBED": null, + "WALKING_RUNNING_DISTANCE": null, + "VO2_MAX": null, + "BODY_TEMPERATURE": null, + "FAT_FREE_MASS": null, + "FAT_MASS": null, + "MUSCLE_MASS": null, + "SKIN_TEMPERATURE": null, + "PULSE_WAVE_VELOCITY": null, + "VASCULAR_AGE": null, + "VISCERAL_FAT": null, + "AUDIO_EXPOSURE_ENV": null, + "AUDIO_EXPOSURE_HEADPHONE": null, + "TIME_IN_DAYLIGHT": null, + "WALKING_STEADINESS": null, + "AUDIO_EXPOSURE_EVENT": null, + "RESPIRATORY_RATE": null, + "BODY_MASS_INDEX": null, + "LEAN_BODY_MASS": null, + "WALKING_HEART_RATE_AVERAGE": null, + "WALKING_ASYMMETRY": null, + "WALKING_DOUBLE_SUPPORT": null, + "WALKING_STEP_LENGTH": null, + "WALKING_SPEED": null, + "CARDIO_RECOVERY": null, + "WRIST_TEMPERATURE": null, + "FALL_COUNT": null, + "SIX_MINUTE_WALK_DISTANCE": null, + "STAIR_ASCENT_SPEED": null, + "STAIR_DESCENT_SPEED": null, + "BREATHING_DISTURBANCES": null, + "IRREGULAR_RHYTHM_NOTIFICATION": null, + "HIGH_HEART_RATE_EVENT": null, + "LOW_HEART_RATE_EVENT": null, + "WALKING_STEADINESS_EVENT": null, + "BREATHING_DISTURBANCE_EVENT": null, + "RECOVERY_SCORE": null, + "STRESS_SCORE": null, + "STRAIN_SCORE": null, + "HRV_RMSSD": null, + "DAY_STRAIN": null, + "WORKOUT_STRAIN": null, + "SLEEP_PERFORMANCE": null, + "SLEEP_EFFICIENCY": null, + "SLEEP_CONSISTENCY": null, + "SLEEP_NEED": null, + "ENERGY_EXPENDITURE_KJ": null, + "AVERAGE_HEART_RATE": null, + "MAX_HEART_RATE": null, + "SLEEP_DISTURBANCE_COUNT": null, + "ANS_CHARGE": null, + "CARDIO_LOAD": null, + "SLEEP_SCORE": null, + "BODY_TEMPERATURE_DEVIATION": null, + "RESILIENCE": null, + "PHQ9_SCORE": null, + "GAD7_SCORE": null, + "GRIP_STRENGTH": null, + "PAIN_NRS": null, + "WAIST_CIRCUMFERENCE": null, + "WAIST_TO_HEIGHT": null, + "WHO5_SCORE": null, + "SCI_SCORE": null, + "EXERCISE_MINUTES": null, + "STAND_MINUTES": null, + "STAND_HOURS": null, + "MINDFUL_MINUTES": null, + "STATE_OF_MIND_VALENCE": null, + "AFIB_BURDEN": null, + "INDOOR_CO2": null, + "INDOOR_TEMPERATURE": null, + "INDOOR_HUMIDITY": null + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "analytics.get" + }, + "meta": { + "cache.analytics.outcome": "hit", + "cache.analytics.key_hash": 364623505 + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "healthScores": [ + { + "dayKey": "day+0", + "timezone": "UTC", + "composite": 86, + "band": "green", + "scoreVersion": 4, + "composition": [ + "BLOOD_PRESSURE", + "GLYCAEMIA", + "ACTIVITY", + "SLEEP" + ], + "pillarScores": { + "SLEEP": 92, + "ACTIVITY": 78, + "GLYCAEMIA": 100, + "BLOOD_PRESSURE": 73 + }, + "inputFingerprint": "5653cd8c051404182d6c6f813a48b1c2ebc3abe1116901466ab7cc1ab70cb276", + "computedAt": "anchor+0d+12:00:00" + } + ], + "patterns": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/analytics.06-summaries-modules-off.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.06-summaries-modules-off.json.snap new file mode 100644 index 000000000..94952320a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/analytics.06-summaries-modules-off.json.snap @@ -0,0 +1,1299 @@ +{ + "status": 200, + "cacheControl": "private, max-age=0, must-revalidate", + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 60, + "latest": 82, + "min": 82, + "max": 84.4, + "mean": 83.18, + "median": 83.2, + "avg7": 82.11, + "avg30": 82.58, + "slope7": { + "slope": -0.042, + "direction": "down", + "confidence": 0.93 + }, + "slope30": { + "slope": -0.04, + "direction": "down", + "confidence": 0.99 + }, + "avg30LastMonth": 83.78, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 91, + "latest": 123, + "min": 123, + "max": 137, + "mean": 129.92, + "median": 130, + "avg7": 131.14, + "avg30": 130, + "slope7": { + "slope": -0.393, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": 0.071, + "direction": "up", + "confidence": 0.02 + }, + "avg30LastMonth": 130, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 91, + "latest": 78, + "min": 78, + "max": 86, + "mean": 81.96, + "median": 82, + "avg7": 81.43, + "avg30": 81.8, + "slope7": { + "slope": -0.714, + "direction": "down", + "confidence": 0.36 + }, + "slope30": { + "slope": -0.017, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": 82.1, + "avg30LastYear": null + }, + "PULSE": { + "count": 60, + "latest": 64, + "min": 64, + "max": 69, + "mean": 66.5, + "median": 66.5, + "avg7": 66.14, + "avg30": 66.5, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": -0.02, + "direction": "down", + "confidence": 0.01 + }, + "avg30LastMonth": 66.5, + "avg30LastYear": null + }, + "BODY_FAT": { + "count": 9, + "latest": 24, + "min": 24, + "max": 25.1, + "mean": 24.56, + "median": 24.6, + "avg7": 24, + "avg30": 24.28, + "slope7": { + "slope": -0.014, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.021, + "direction": "down", + "confidence": 0.99 + }, + "avg30LastMonth": 24.9, + "avg30LastYear": null + }, + "ACTIVITY_STEPS": { + "count": 30, + "latest": 6000, + "min": 6000, + "max": 9833, + "mean": 7808.5, + "median": 7781.5, + "avg7": 7119, + "avg30": 7808.5, + "slope7": { + "slope": -373, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -30.397, + "direction": "down", + "confidence": 0.06 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TOTAL_BODY_WATER": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BONE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "OXYGEN_SATURATION": { + "count": 10, + "latest": 96, + "min": 96, + "max": 98, + "mean": 96.9, + "median": 97, + "avg7": 96.86, + "avg30": 96.9, + "slope7": { + "slope": -0.036, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.018, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HEART_RATE_VARIABILITY": { + "count": 30, + "latest": 45, + "min": 45, + "max": 52, + "mean": 48.3, + "median": 48, + "avg7": 48, + "avg30": 48.3, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.032, + "direction": "down", + "confidence": 0.02 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVE_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FLIGHTS_CLIMBED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_RUNNING_DISTANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VO2_MAX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_FREE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MUSCLE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SKIN_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PULSE_WAVE_VELOCITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VASCULAR_AGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VISCERAL_FAT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_ENV": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_HEADPHONE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TIME_IN_DAYLIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESPIRATORY_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_MASS_INDEX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LEAN_BODY_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_HEART_RATE_AVERAGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_ASYMMETRY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_DOUBLE_SUPPORT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEP_LENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WRIST_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FALL_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "IRREGULAR_RHYTHM_NOTIFICATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HIGH_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LOW_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BREATHING_DISTURBANCE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RECOVERY_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRESS_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRAIN_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "DAY_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WORKOUT_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_PERFORMANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_EFFICIENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_CONSISTENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_NEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ENERGY_EXPENDITURE_KJ": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AVERAGE_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MAX_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_DISTURBANCE_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ANS_CHARGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "CARDIO_LOAD": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE_DEVIATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESILIENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PHQ9_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GAD7_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GRIP_STRENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PAIN_NRS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_CIRCUMFERENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_TO_HEIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WHO5_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SCI_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "EXERCISE_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_HOURS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MINDFUL_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STATE_OF_MIND_VALENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AFIB_BURDEN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_CO2": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_HUMIDITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + } + }, + "bmi": null, + "lastSeenByType": { + "WEIGHT": { + "lastSeenAt": "anchor+0d+06:30:00", + "daysAgo": 0 + }, + "BLOOD_PRESSURE_SYS": { + "lastSeenAt": "anchor+0d+07:00:00", + "daysAgo": 0 + }, + "BLOOD_PRESSURE_DIA": { + "lastSeenAt": "anchor+0d+07:00:00", + "daysAgo": 0 + }, + "PULSE": { + "lastSeenAt": "anchor+0d+07:01:00", + "daysAgo": 0 + }, + "BODY_FAT": { + "lastSeenAt": "anchor+0d+06:31:00", + "daysAgo": 0 + }, + "ACTIVITY_STEPS": { + "lastSeenAt": "anchor+0d+09:00:00", + "daysAgo": 0 + }, + "TOTAL_BODY_WATER": null, + "BONE_MASS": null, + "OXYGEN_SATURATION": { + "lastSeenAt": "anchor+0d+04:40:00", + "daysAgo": 0 + }, + "HEART_RATE_VARIABILITY": { + "lastSeenAt": "anchor+0d+04:30:00", + "daysAgo": 0 + }, + "ACTIVE_ENERGY_BURNED": null, + "FLIGHTS_CLIMBED": null, + "WALKING_RUNNING_DISTANCE": null, + "VO2_MAX": null, + "BODY_TEMPERATURE": null, + "FAT_FREE_MASS": null, + "FAT_MASS": null, + "MUSCLE_MASS": null, + "SKIN_TEMPERATURE": null, + "PULSE_WAVE_VELOCITY": null, + "VASCULAR_AGE": null, + "VISCERAL_FAT": null, + "AUDIO_EXPOSURE_ENV": null, + "AUDIO_EXPOSURE_HEADPHONE": null, + "TIME_IN_DAYLIGHT": null, + "WALKING_STEADINESS": null, + "AUDIO_EXPOSURE_EVENT": null, + "RESPIRATORY_RATE": null, + "BODY_MASS_INDEX": null, + "LEAN_BODY_MASS": null, + "WALKING_HEART_RATE_AVERAGE": null, + "WALKING_ASYMMETRY": null, + "WALKING_DOUBLE_SUPPORT": null, + "WALKING_STEP_LENGTH": null, + "WALKING_SPEED": null, + "WRIST_TEMPERATURE": null, + "FALL_COUNT": null, + "IRREGULAR_RHYTHM_NOTIFICATION": null, + "HIGH_HEART_RATE_EVENT": null, + "LOW_HEART_RATE_EVENT": null, + "WALKING_STEADINESS_EVENT": null, + "BREATHING_DISTURBANCE_EVENT": null, + "RECOVERY_SCORE": null, + "STRESS_SCORE": null, + "STRAIN_SCORE": null, + "DAY_STRAIN": null, + "WORKOUT_STRAIN": null, + "SLEEP_PERFORMANCE": null, + "SLEEP_EFFICIENCY": null, + "SLEEP_CONSISTENCY": null, + "SLEEP_NEED": null, + "ENERGY_EXPENDITURE_KJ": null, + "AVERAGE_HEART_RATE": null, + "MAX_HEART_RATE": null, + "SLEEP_DISTURBANCE_COUNT": null, + "ANS_CHARGE": null, + "CARDIO_LOAD": null, + "SLEEP_SCORE": null, + "BODY_TEMPERATURE_DEVIATION": null, + "RESILIENCE": null, + "PHQ9_SCORE": null, + "GAD7_SCORE": null, + "GRIP_STRENGTH": null, + "PAIN_NRS": null, + "WAIST_CIRCUMFERENCE": null, + "WAIST_TO_HEIGHT": null, + "WHO5_SCORE": null, + "SCI_SCORE": null, + "EXERCISE_MINUTES": null, + "STAND_MINUTES": null, + "STAND_HOURS": null, + "MINDFUL_MINUTES": null, + "STATE_OF_MIND_VALENCE": null, + "AFIB_BURDEN": null, + "INDOOR_CO2": null, + "INDOOR_TEMPERATURE": null, + "INDOOR_HUMIDITY": null + }, + "sleepSourceDiscrepancy": null, + "recoveryEmpty": "general" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "analytics.get" + }, + "meta": { + "cache.summaries_slice.outcome": "hit" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "healthScores": [ + { + "dayKey": "day+0", + "timezone": "UTC", + "composite": 86, + "band": "green", + "scoreVersion": 4, + "composition": [ + "BLOOD_PRESSURE", + "GLYCAEMIA", + "ACTIVITY", + "SLEEP" + ], + "pillarScores": { + "SLEEP": 92, + "ACTIVITY": 78, + "GLYCAEMIA": 100, + "BLOOD_PRESSURE": 73 + }, + "inputFingerprint": "5653cd8c051404182d6c6f813a48b1c2ebc3abe1116901466ab7cc1ab70cb276", + "computedAt": "anchor+0d+12:00:00" + } + ], + "patterns": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/analytics.07-default-de-personal-target.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.07-default-de-personal-target.json.snap new file mode 100644 index 000000000..5c93599a8 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/analytics.07-default-de-personal-target.json.snap @@ -0,0 +1,2169 @@ +{ + "status": 200, + "cacheControl": "private, max-age=0, must-revalidate", + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 60, + "latest": 82, + "min": 82, + "max": 84.4, + "mean": 83.18, + "median": 83.2, + "avg7": 82.11, + "avg30": 82.58, + "slope7": { + "slope": -0.042, + "direction": "down", + "confidence": 0.93 + }, + "slope30": { + "slope": -0.04, + "direction": "down", + "confidence": 0.99 + }, + "avg30LastMonth": 83.78, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 91, + "latest": 123, + "min": 123, + "max": 137, + "mean": 129.92, + "median": 130, + "avg7": 131.14, + "avg30": 130, + "slope7": { + "slope": -0.393, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": 0.071, + "direction": "up", + "confidence": 0.02 + }, + "avg30LastMonth": 130, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 91, + "latest": 78, + "min": 78, + "max": 86, + "mean": 81.96, + "median": 82, + "avg7": 81.43, + "avg30": 81.8, + "slope7": { + "slope": -0.714, + "direction": "down", + "confidence": 0.36 + }, + "slope30": { + "slope": -0.017, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": 82.1, + "avg30LastYear": null + }, + "PULSE": { + "count": 60, + "latest": 64, + "min": 64, + "max": 69, + "mean": 66.5, + "median": 66.5, + "avg7": 66.14, + "avg30": 66.5, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": -0.02, + "direction": "down", + "confidence": 0.01 + }, + "avg30LastMonth": 66.5, + "avg30LastYear": null + }, + "BODY_FAT": { + "count": 9, + "latest": 24, + "min": 24, + "max": 25.1, + "mean": 24.56, + "median": 24.6, + "avg7": 24, + "avg30": 24.28, + "slope7": { + "slope": -0.014, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.021, + "direction": "down", + "confidence": 0.99 + }, + "avg30LastMonth": 24.9, + "avg30LastYear": null + }, + "ACTIVITY_STEPS": { + "count": 30, + "latest": 6000, + "min": 6000, + "max": 9833, + "mean": 7808.5, + "median": 7781.5, + "avg7": 7119, + "avg30": 7808.5, + "slope7": { + "slope": -373, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -30.397, + "direction": "down", + "confidence": 0.06 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TOTAL_BODY_WATER": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BONE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "OXYGEN_SATURATION": { + "count": 10, + "latest": 96, + "min": 96, + "max": 98, + "mean": 96.9, + "median": 97, + "avg7": 96.86, + "avg30": 96.9, + "slope7": { + "slope": -0.036, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.018, + "direction": "down", + "confidence": 0 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HEART_RATE_VARIABILITY": { + "count": 30, + "latest": 45, + "min": 45, + "max": 52, + "mean": 48.3, + "median": 48, + "avg7": 48, + "avg30": 48.3, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.032, + "direction": "down", + "confidence": 0.02 + }, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVE_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FLIGHTS_CLIMBED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_RUNNING_DISTANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VO2_MAX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_FREE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MUSCLE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SKIN_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PULSE_WAVE_VELOCITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VASCULAR_AGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VISCERAL_FAT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_ENV": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_HEADPHONE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TIME_IN_DAYLIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESPIRATORY_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_MASS_INDEX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LEAN_BODY_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_HEART_RATE_AVERAGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_ASYMMETRY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_DOUBLE_SUPPORT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEP_LENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WRIST_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FALL_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "IRREGULAR_RHYTHM_NOTIFICATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HIGH_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LOW_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BREATHING_DISTURBANCE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RECOVERY_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRESS_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRAIN_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "DAY_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WORKOUT_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_PERFORMANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_EFFICIENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_CONSISTENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_NEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ENERGY_EXPENDITURE_KJ": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AVERAGE_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MAX_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_DISTURBANCE_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ANS_CHARGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "CARDIO_LOAD": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE_DEVIATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESILIENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PHQ9_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GAD7_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GRIP_STRENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PAIN_NRS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_CIRCUMFERENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_TO_HEIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WHO5_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SCI_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "EXERCISE_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_HOURS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MINDFUL_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STATE_OF_MIND_VALENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AFIB_BURDEN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_CO2": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_HUMIDITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + } + }, + "bmi": 25.9, + "bpInTargetPct": 7, + "bpInTargetPct7d": 14, + "bpInTargetPct30d": 10, + "bpInTargetPctAllTime": 8, + "bpInTargetPctPriorMonth": 10, + "bpInTargetPctPriorYear": null, + "bpInTargetCount90": 90, + "bpInTargetSpanDays90": 90, + "glucoseByContext": null, + "glucoseClinical": null, + "correlations": { + "bpCompliance": { + "kind": "bp-compliance", + "status": "insufficient", + "n": 24, + "reason": "not_significant", + "points": [ + { + "x": 100, + "y": 125 + }, + { + "x": 100, + "y": 133 + }, + { + "x": 100, + "y": 126 + }, + { + "x": 100, + "y": 134 + }, + { + "x": 100, + "y": 127 + }, + { + "x": 0, + "y": 135 + }, + { + "x": 100, + "y": 128 + }, + { + "x": 100, + "y": 136 + }, + { + "x": 100, + "y": 137 + }, + { + "x": 100, + "y": 130 + }, + { + "x": 100, + "y": 123 + }, + { + "x": 50, + "y": 131 + }, + { + "x": 100, + "y": 124 + }, + { + "x": 100, + "y": 132 + }, + { + "x": 100, + "y": 125 + }, + { + "x": 100, + "y": 133 + }, + { + "x": 100, + "y": 134 + }, + { + "x": 0, + "y": 127 + }, + { + "x": 100, + "y": 135 + }, + { + "x": 100, + "y": 128 + }, + { + "x": 100, + "y": 136 + }, + { + "x": 100, + "y": 129 + }, + { + "x": 100, + "y": 137 + }, + { + "x": 100, + "y": 130 + } + ] + }, + "moodPulse": { + "kind": "mood-pulse", + "status": "insufficient", + "n": 28, + "reason": "not_significant", + "points": [ + { + "x": 1, + "y": 58 + }, + { + "x": 4, + "y": 59 + }, + { + "x": 2, + "y": 60 + }, + { + "x": 5, + "y": 61 + }, + { + "x": 3, + "y": 58 + }, + { + "x": 1, + "y": 59 + }, + { + "x": 4, + "y": 60 + }, + { + "x": 2, + "y": 61 + }, + { + "x": 5, + "y": 58 + }, + { + "x": 3, + "y": 59 + }, + { + "x": 1, + "y": 60 + }, + { + "x": 4, + "y": 61 + }, + { + "x": 2, + "y": 58 + }, + { + "x": 5, + "y": 59 + }, + { + "x": 3, + "y": 60 + }, + { + "x": 1, + "y": 61 + }, + { + "x": 4, + "y": 58 + }, + { + "x": 2, + "y": 59 + }, + { + "x": 5, + "y": 60 + }, + { + "x": 3, + "y": 61 + }, + { + "x": 1, + "y": 58 + }, + { + "x": 4, + "y": 59 + }, + { + "x": 2, + "y": 60 + }, + { + "x": 5, + "y": 61 + }, + { + "x": 3, + "y": 58 + }, + { + "x": 1, + "y": 59 + }, + { + "x": 4, + "y": 60 + }, + { + "x": 2, + "y": 61 + } + ] + }, + "weightWeekday": { + "kind": "weight-weekday", + "status": "insufficient", + "n": 28, + "reason": "not_significant", + "points": [ + { + "x": 5, + "y": 83.1 + }, + { + "x": 6, + "y": 83 + }, + { + "x": 0, + "y": 83 + }, + { + "x": 1, + "y": 83 + }, + { + "x": 2, + "y": 82.9 + }, + { + "x": 3, + "y": 82.9 + }, + { + "x": 4, + "y": 82.8 + }, + { + "x": 5, + "y": 82.8 + }, + { + "x": 6, + "y": 82.8 + }, + { + "x": 0, + "y": 82.7 + }, + { + "x": 1, + "y": 82.7 + }, + { + "x": 2, + "y": 82.6 + }, + { + "x": 3, + "y": 82.6 + }, + { + "x": 4, + "y": 82.6 + }, + { + "x": 5, + "y": 82.5 + }, + { + "x": 6, + "y": 82.5 + }, + { + "x": 0, + "y": 82.4 + }, + { + "x": 1, + "y": 82.4 + }, + { + "x": 2, + "y": 82.4 + }, + { + "x": 3, + "y": 82.3 + }, + { + "x": 4, + "y": 82.3 + }, + { + "x": 5, + "y": 82.2 + }, + { + "x": 6, + "y": 82.2 + }, + { + "x": 0, + "y": 82.2 + }, + { + "x": 1, + "y": 82.1 + }, + { + "x": 2, + "y": 82.1 + }, + { + "x": 3, + "y": 82 + }, + { + "x": 4, + "y": 82 + } + ] + }, + "degraded": false, + "windowDays": 28, + "path": "rollup" + }, + "healthScore": { + "composite": { + "status": "ok", + "value": { + "score": 76, + "band": "green", + "bandSetter": null, + "composition": [ + "BLOOD_PRESSURE", + "ACTIVITY" + ], + "configured": false, + "scoreBasis": { + "domains": 2, + "recommended": 3, + "tier": "partial", + "physiological": true + }, + "noiseFloor": 3, + "scoreVersion": 4 + }, + "coverage": { + "requiredInputs": 3, + "presentInputs": 2, + "historyDays": 28, + "missing": [ + "adiposity", + "wellbeing" + ] + }, + "confidence": { + "score": 67, + "band": "medium" + }, + "provenance": { + "inputs": [ + "BLOOD_PRESSURE", + "ACTIVITY" + ], + "source": "live", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + } + }, + "pillars": [ + { + "id": "BLOOD_PRESSURE", + "domain": "cardiometabolic", + "result": { + "status": "ok", + "value": { + "score": 73, + "scoreBasis": { + "axis": "diastolic", + "relation": "above_ceiling", + "offsetMmHg": 3, + "boundaryMmHg": 79 + }, + "observed": { + "value": 130, + "unit": "mmHg", + "label": "130/82 mmHg", + "asOf": "anchor+0d+00:00:00", + "sources": [ + "MANUAL" + ] + }, + "reference": { + "kind": "clinical-threshold", + "low": 120, + "high": 129, + "label": "120–129/70–79 mmHg", + "source": "ESH 2023" + }, + "personalReference": { + "kind": "guideline-band", + "low": 100, + "high": 125, + "label": "100–125/60–80 mmHg", + "source": "personal target" + }, + "noiseFloor": 1, + "deltaEligible": true, + "deltaIdentity": "graded_bp_pair_series" + }, + "coverage": { + "requiredInputs": 12, + "presentInputs": 12, + "historyDays": 90, + "missing": [] + }, + "confidence": { + "score": 100, + "band": "high" + }, + "provenance": { + "inputs": [ + "BLOOD_PRESSURE" + ], + "source": "DAY", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + } + } + }, + { + "id": "GLYCAEMIA", + "domain": "cardiometabolic", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 8, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "8 fasting readings" + ] + }, + "provenance": { + "inputs": [ + "GLYCAEMIA" + ], + "source": "live", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + }, + { + "id": "ACTIVITY", + "domain": "activity", + "result": { + "status": "ok", + "value": { + "score": 78, + "observed": { + "value": 7837, + "unit": "steps/day", + "label": "7,837 steps/day", + "asOf": "anchor-1d+00:00:00", + "sources": [ + "APPLE_HEALTH" + ] + }, + "reference": { + "kind": "guideline-band", + "low": 0, + "high": 10000, + "label": "benefit plateaus by 10,000 steps/day", + "source": "Paluch 2022" + }, + "noiseFloor": 5, + "deltaEligible": true, + "deltaIdentity": "ACTIVITY_STEPS" + }, + "coverage": { + "requiredInputs": 21, + "presentInputs": 21, + "historyDays": 28, + "missing": [] + }, + "confidence": { + "score": 100, + "band": "high" + }, + "provenance": { + "inputs": [ + "ACTIVITY" + ], + "source": "live", + "windowDays": 28, + "computedAt": "anchor+0d+12:00:00" + } + } + }, + { + "id": "ADIPOSITY", + "domain": "adiposity", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 1, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "waist measurement" + ] + }, + "provenance": { + "inputs": [ + "ADIPOSITY" + ], + "source": "live", + "windowDays": 180, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + }, + { + "id": "WELLBEING", + "domain": "wellbeing", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 1, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "completed WHO-5, PHQ-9 or GAD-7" + ] + }, + "provenance": { + "inputs": [ + "WELLBEING" + ], + "source": "live", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + }, + { + "id": "LIPIDS", + "domain": "cardiometabolic", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 1, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "lipid panel with lab reference ranges" + ] + }, + "provenance": { + "inputs": [ + "LIPIDS" + ], + "source": "live", + "windowDays": 365, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + } + ], + "delta": null, + "deltaReason": "first_eligibility_window", + "scoreVersion": 4, + "weightGoal": { + "status": "insufficient", + "coverage": { + "requiredInputs": 2, + "presentInputs": 1, + "historyDays": 1, + "missing": [ + "personal weight target" + ] + }, + "provenance": { + "inputs": [ + "WEIGHT", + "personal weight target" + ], + "source": "live", + "windowDays": 60, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "no_personal_goal" + }, + "algorithmNotice": { + "itemKey": "health_score_algorithm:4", + "dismissed": false + }, + "compositionNotice": null, + "restMode": { + "active": true, + "since": "anchor-3d+12:00:00", + "episodeCount": 1 + } + }, + "sleepStages": { + "windowDays": 30, + "nights": 21, + "totalMinutes": 8362, + "stages": { + "CORE": 4600, + "DEEP": 1575, + "REM": 1935, + "AWAKE": 252 + }, + "perNight": [ + { + "dayKey": "day-20", + "stages": { + "CORE": 200, + "DEEP": 80, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-19", + "stages": { + "CORE": 240, + "DEEP": 75, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-18", + "stages": { + "CORE": 230, + "DEEP": 70, + "REM": 95, + "AWAKE": 12 + } + }, + { + "dayKey": "day-17", + "stages": { + "CORE": 220, + "DEEP": 80, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day-16", + "stages": { + "CORE": 210, + "DEEP": 75, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-15", + "stages": { + "CORE": 200, + "DEEP": 70, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-14", + "stages": { + "CORE": 240, + "DEEP": 80, + "REM": 95, + "AWAKE": 12 + } + }, + { + "dayKey": "day-13", + "stages": { + "CORE": 230, + "DEEP": 75, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day-12", + "stages": { + "CORE": 220, + "DEEP": 70, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-11", + "stages": { + "CORE": 210, + "DEEP": 80, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-10", + "stages": { + "CORE": 200, + "DEEP": 75, + "REM": 95, + "AWAKE": 12 + } + }, + { + "dayKey": "day-9", + "stages": { + "CORE": 240, + "DEEP": 70, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day-8", + "stages": { + "CORE": 230, + "DEEP": 80, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-7", + "stages": { + "CORE": 220, + "DEEP": 75, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-6", + "stages": { + "CORE": 210, + "DEEP": 70, + "REM": 95, + "AWAKE": 12 + } + }, + { + "dayKey": "day-5", + "stages": { + "CORE": 200, + "DEEP": 80, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day-4", + "stages": { + "CORE": 240, + "DEEP": 75, + "REM": 85, + "AWAKE": 12 + } + }, + { + "dayKey": "day-3", + "stages": { + "CORE": 230, + "DEEP": 70, + "REM": 100, + "AWAKE": 12 + } + }, + { + "dayKey": "day-2", + "stages": { + "CORE": 220, + "DEEP": 80, + "REM": 95, + "AWAKE": 12 + }, + "napMinutes": 35, + "napCount": 1 + }, + { + "dayKey": "day-1", + "stages": { + "CORE": 210, + "DEEP": 75, + "REM": 90, + "AWAKE": 12 + } + }, + { + "dayKey": "day+0", + "stages": { + "CORE": 200, + "DEEP": 70, + "REM": 85, + "AWAKE": 12 + } + } + ] + }, + "sleepSourceDiscrepancy": null, + "lastSeenByType": { + "WEIGHT": { + "lastSeenAt": "anchor+0d+06:30:00", + "daysAgo": 0 + }, + "BLOOD_PRESSURE_SYS": { + "lastSeenAt": "anchor+0d+07:00:00", + "daysAgo": 0 + }, + "BLOOD_PRESSURE_DIA": { + "lastSeenAt": "anchor+0d+07:00:00", + "daysAgo": 0 + }, + "PULSE": { + "lastSeenAt": "anchor+0d+07:01:00", + "daysAgo": 0 + }, + "BODY_FAT": { + "lastSeenAt": "anchor+0d+06:31:00", + "daysAgo": 0 + }, + "ACTIVITY_STEPS": { + "lastSeenAt": "anchor+0d+09:00:00", + "daysAgo": 0 + }, + "TOTAL_BODY_WATER": null, + "BONE_MASS": null, + "OXYGEN_SATURATION": { + "lastSeenAt": "anchor+0d+04:40:00", + "daysAgo": 0 + }, + "HEART_RATE_VARIABILITY": { + "lastSeenAt": "anchor+0d+04:30:00", + "daysAgo": 0 + }, + "ACTIVE_ENERGY_BURNED": null, + "FLIGHTS_CLIMBED": null, + "WALKING_RUNNING_DISTANCE": null, + "VO2_MAX": null, + "BODY_TEMPERATURE": null, + "FAT_FREE_MASS": null, + "FAT_MASS": null, + "MUSCLE_MASS": null, + "SKIN_TEMPERATURE": null, + "PULSE_WAVE_VELOCITY": null, + "VASCULAR_AGE": null, + "VISCERAL_FAT": null, + "AUDIO_EXPOSURE_ENV": null, + "AUDIO_EXPOSURE_HEADPHONE": null, + "TIME_IN_DAYLIGHT": null, + "WALKING_STEADINESS": null, + "AUDIO_EXPOSURE_EVENT": null, + "RESPIRATORY_RATE": null, + "BODY_MASS_INDEX": null, + "LEAN_BODY_MASS": null, + "WALKING_HEART_RATE_AVERAGE": null, + "WALKING_ASYMMETRY": null, + "WALKING_DOUBLE_SUPPORT": null, + "WALKING_STEP_LENGTH": null, + "WALKING_SPEED": null, + "WRIST_TEMPERATURE": null, + "FALL_COUNT": null, + "IRREGULAR_RHYTHM_NOTIFICATION": null, + "HIGH_HEART_RATE_EVENT": null, + "LOW_HEART_RATE_EVENT": null, + "WALKING_STEADINESS_EVENT": null, + "BREATHING_DISTURBANCE_EVENT": null, + "RECOVERY_SCORE": null, + "STRESS_SCORE": null, + "STRAIN_SCORE": null, + "DAY_STRAIN": null, + "WORKOUT_STRAIN": null, + "SLEEP_PERFORMANCE": null, + "SLEEP_EFFICIENCY": null, + "SLEEP_CONSISTENCY": null, + "SLEEP_NEED": null, + "ENERGY_EXPENDITURE_KJ": null, + "AVERAGE_HEART_RATE": null, + "MAX_HEART_RATE": null, + "SLEEP_DISTURBANCE_COUNT": null, + "ANS_CHARGE": null, + "CARDIO_LOAD": null, + "SLEEP_SCORE": null, + "BODY_TEMPERATURE_DEVIATION": null, + "RESILIENCE": null, + "PHQ9_SCORE": null, + "GAD7_SCORE": null, + "GRIP_STRENGTH": null, + "PAIN_NRS": null, + "WAIST_CIRCUMFERENCE": null, + "WAIST_TO_HEIGHT": null, + "WHO5_SCORE": null, + "SCI_SCORE": null, + "EXERCISE_MINUTES": null, + "STAND_MINUTES": null, + "STAND_HOURS": null, + "MINDFUL_MINUTES": null, + "STATE_OF_MIND_VALENCE": null, + "AFIB_BURDEN": null, + "INDOOR_CO2": null, + "INDOOR_TEMPERATURE": null, + "INDOOR_HUMIDITY": null + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "analytics.get" + }, + "meta": { + "cache.summaries_slice.outcome": "hit", + "analytics": { + "bp_in_target": { + "path": "rollup", + "tz_guard": "near-utc" + } + }, + "correlations": { + "bpCompliance": "insufficient", + "moodPulse": "insufficient", + "weightWeekday": "insufficient", + "path": "rollup", + "window_days": 28, + "degraded": false, + "tz_guard": "near-utc" + }, + "cache.analytics.outcome": "miss", + "cache.analytics.key_hash": 364623463 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserRollupsFresh", + "args": [ + "" + ] + } + ], + "stored": { + "healthScores": [ + { + "dayKey": "day+0", + "timezone": "UTC", + "composite": 86, + "band": "green", + "scoreVersion": 4, + "composition": [ + "BLOOD_PRESSURE", + "GLYCAEMIA", + "ACTIVITY", + "SLEEP" + ], + "pillarScores": { + "SLEEP": 92, + "ACTIVITY": 78, + "GLYCAEMIA": 100, + "BLOOD_PRESSURE": 73 + }, + "inputFingerprint": "5653cd8c051404182d6c6f813a48b1c2ebc3abe1116901466ab7cc1ab70cb276", + "computedAt": "anchor+0d+12:00:00" + } + ], + "patterns": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/analytics.08-empty-account.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.08-empty-account.json.snap new file mode 100644 index 000000000..9521b5107 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/analytics.08-empty-account.json.snap @@ -0,0 +1,1630 @@ +{ + "status": 200, + "cacheControl": "private, max-age=0, must-revalidate", + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PULSE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_FAT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_DURATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVITY_STEPS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BLOOD_GLUCOSE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TOTAL_BODY_WATER": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BONE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "OXYGEN_SATURATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HEART_RATE_VARIABILITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESTING_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVE_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FLIGHTS_CLIMBED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_RUNNING_DISTANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VO2_MAX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_FREE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FAT_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MUSCLE_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SKIN_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PULSE_WAVE_VELOCITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VASCULAR_AGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "VISCERAL_FAT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_ENV": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_HEADPHONE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "TIME_IN_DAYLIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AUDIO_EXPOSURE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESPIRATORY_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_MASS_INDEX": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LEAN_BODY_MASS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_HEART_RATE_AVERAGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_ASYMMETRY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_DOUBLE_SUPPORT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEP_LENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "CARDIO_RECOVERY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WRIST_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "FALL_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SIX_MINUTE_WALK_DISTANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAIR_ASCENT_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAIR_DESCENT_SPEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BREATHING_DISTURBANCES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "IRREGULAR_RHYTHM_NOTIFICATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HIGH_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "LOW_HEART_RATE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WALKING_STEADINESS_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BREATHING_DISTURBANCE_EVENT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RECOVERY_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRESS_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STRAIN_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HRV_RMSSD": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "DAY_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WORKOUT_STRAIN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_PERFORMANCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_EFFICIENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_CONSISTENCY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_NEED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ENERGY_EXPENDITURE_KJ": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AVERAGE_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MAX_HEART_RATE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_DISTURBANCE_COUNT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ANS_CHARGE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "CARDIO_LOAD": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SLEEP_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BODY_TEMPERATURE_DEVIATION": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESILIENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PHQ9_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GAD7_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "GRIP_STRENGTH": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "PAIN_NRS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_CIRCUMFERENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WAIST_TO_HEIGHT": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "WHO5_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "SCI_SCORE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "EXERCISE_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STAND_HOURS": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "MINDFUL_MINUTES": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "STATE_OF_MIND_VALENCE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "AFIB_BURDEN": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_CO2": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_TEMPERATURE": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "INDOOR_HUMIDITY": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null + } + }, + "bmi": null, + "bpInTargetPct": null, + "bpInTargetPct7d": null, + "bpInTargetPct30d": null, + "bpInTargetPctAllTime": null, + "bpInTargetPctPriorMonth": null, + "bpInTargetPctPriorYear": null, + "bpInTargetCount90": null, + "bpInTargetSpanDays90": null, + "glucoseByContext": {}, + "glucoseClinical": { + "stillLearning": true, + "stillLearningReason": "No glucose readings in the last 30 days.", + "windowDays": 30, + "actualSpanDays": 0, + "readingCount": 0, + "meanMgdl": null, + "distribution": null, + "gmi": null, + "estimatedA1c": null, + "variability": null, + "advanced": null, + "isSpotEstimate": true + }, + "correlations": { + "bpCompliance": { + "kind": "bp-compliance", + "status": "insufficient", + "n": 0, + "reason": "too_few_pairs", + "points": [] + }, + "moodPulse": { + "kind": "mood-pulse", + "status": "insufficient", + "n": 0, + "reason": "too_few_pairs", + "points": [] + }, + "weightWeekday": { + "kind": "weight-weekday", + "status": "insufficient", + "n": 0, + "reason": "too_few_pairs", + "points": [] + }, + "degraded": false, + "windowDays": 28, + "path": "live" + }, + "healthScore": { + "composite": { + "status": "insufficient", + "coverage": { + "requiredInputs": 3, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "cardiometabolic", + "activity", + "sleep", + "adiposity", + "wellbeing" + ] + }, + "provenance": { + "inputs": [], + "source": "none", + "windowDays": 0, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "no_usable_data" + }, + "pillars": [ + { + "id": "BLOOD_PRESSURE", + "domain": "cardiometabolic", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 12, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "date of birth" + ] + }, + "provenance": { + "inputs": [ + "BLOOD_PRESSURE" + ], + "source": "live", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "missing_reference_profile" + } + }, + { + "id": "GLYCAEMIA", + "domain": "cardiometabolic", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 8, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "8 fasting readings" + ] + }, + "provenance": { + "inputs": [ + "GLYCAEMIA" + ], + "source": "live", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + }, + { + "id": "ACTIVITY", + "domain": "activity", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 21, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "21 days" + ] + }, + "provenance": { + "inputs": [ + "ACTIVITY" + ], + "source": "live", + "windowDays": 28, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + }, + { + "id": "SLEEP", + "domain": "sleep", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 14, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "14 nights" + ] + }, + "provenance": { + "inputs": [ + "SLEEP" + ], + "source": "live", + "windowDays": 28, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + }, + { + "id": "ADIPOSITY", + "domain": "adiposity", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 1, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "waist measurement" + ] + }, + "provenance": { + "inputs": [ + "ADIPOSITY" + ], + "source": "live", + "windowDays": 180, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + }, + { + "id": "WELLBEING", + "domain": "wellbeing", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 1, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "completed WHO-5, PHQ-9 or GAD-7" + ] + }, + "provenance": { + "inputs": [ + "WELLBEING" + ], + "source": "live", + "windowDays": 90, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + }, + { + "id": "LIPIDS", + "domain": "cardiometabolic", + "result": { + "status": "insufficient", + "coverage": { + "requiredInputs": 1, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "lipid panel with lab reference ranges" + ] + }, + "provenance": { + "inputs": [ + "LIPIDS" + ], + "source": "live", + "windowDays": 365, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "not_tracked" + } + } + ], + "delta": null, + "deltaReason": "no_current_score", + "scoreVersion": 4, + "weightGoal": { + "status": "insufficient", + "coverage": { + "requiredInputs": 2, + "presentInputs": 0, + "historyDays": 0, + "missing": [ + "weight reading", + "personal weight target" + ] + }, + "provenance": { + "inputs": [ + "WEIGHT", + "personal weight target" + ], + "source": "live", + "windowDays": 60, + "computedAt": "anchor+0d+12:00:00" + }, + "reason": "no_personal_goal" + }, + "algorithmNotice": { + "itemKey": "health_score_algorithm:4", + "dismissed": false + }, + "compositionNotice": null, + "restMode": null + }, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "lastSeenByType": { + "WEIGHT": null, + "BLOOD_PRESSURE_SYS": null, + "BLOOD_PRESSURE_DIA": null, + "PULSE": null, + "BODY_FAT": null, + "SLEEP_DURATION": null, + "ACTIVITY_STEPS": null, + "BLOOD_GLUCOSE": null, + "TOTAL_BODY_WATER": null, + "BONE_MASS": null, + "OXYGEN_SATURATION": null, + "HEART_RATE_VARIABILITY": null, + "RESTING_HEART_RATE": null, + "ACTIVE_ENERGY_BURNED": null, + "FLIGHTS_CLIMBED": null, + "WALKING_RUNNING_DISTANCE": null, + "VO2_MAX": null, + "BODY_TEMPERATURE": null, + "FAT_FREE_MASS": null, + "FAT_MASS": null, + "MUSCLE_MASS": null, + "SKIN_TEMPERATURE": null, + "PULSE_WAVE_VELOCITY": null, + "VASCULAR_AGE": null, + "VISCERAL_FAT": null, + "AUDIO_EXPOSURE_ENV": null, + "AUDIO_EXPOSURE_HEADPHONE": null, + "TIME_IN_DAYLIGHT": null, + "WALKING_STEADINESS": null, + "AUDIO_EXPOSURE_EVENT": null, + "RESPIRATORY_RATE": null, + "BODY_MASS_INDEX": null, + "LEAN_BODY_MASS": null, + "WALKING_HEART_RATE_AVERAGE": null, + "WALKING_ASYMMETRY": null, + "WALKING_DOUBLE_SUPPORT": null, + "WALKING_STEP_LENGTH": null, + "WALKING_SPEED": null, + "CARDIO_RECOVERY": null, + "WRIST_TEMPERATURE": null, + "FALL_COUNT": null, + "SIX_MINUTE_WALK_DISTANCE": null, + "STAIR_ASCENT_SPEED": null, + "STAIR_DESCENT_SPEED": null, + "BREATHING_DISTURBANCES": null, + "IRREGULAR_RHYTHM_NOTIFICATION": null, + "HIGH_HEART_RATE_EVENT": null, + "LOW_HEART_RATE_EVENT": null, + "WALKING_STEADINESS_EVENT": null, + "BREATHING_DISTURBANCE_EVENT": null, + "RECOVERY_SCORE": null, + "STRESS_SCORE": null, + "STRAIN_SCORE": null, + "HRV_RMSSD": null, + "DAY_STRAIN": null, + "WORKOUT_STRAIN": null, + "SLEEP_PERFORMANCE": null, + "SLEEP_EFFICIENCY": null, + "SLEEP_CONSISTENCY": null, + "SLEEP_NEED": null, + "ENERGY_EXPENDITURE_KJ": null, + "AVERAGE_HEART_RATE": null, + "MAX_HEART_RATE": null, + "SLEEP_DISTURBANCE_COUNT": null, + "ANS_CHARGE": null, + "CARDIO_LOAD": null, + "SLEEP_SCORE": null, + "BODY_TEMPERATURE_DEVIATION": null, + "RESILIENCE": null, + "PHQ9_SCORE": null, + "GAD7_SCORE": null, + "GRIP_STRENGTH": null, + "PAIN_NRS": null, + "WAIST_CIRCUMFERENCE": null, + "WAIST_TO_HEIGHT": null, + "WHO5_SCORE": null, + "SCI_SCORE": null, + "EXERCISE_MINUTES": null, + "STAND_MINUTES": null, + "STAND_HOURS": null, + "MINDFUL_MINUTES": null, + "STATE_OF_MIND_VALENCE": null, + "AFIB_BURDEN": null, + "INDOOR_CO2": null, + "INDOOR_TEMPERATURE": null, + "INDOOR_HUMIDITY": null + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "analytics.get" + }, + "meta": { + "analytics": { + "slim_summaries": { + "row_count": 0, + "type_count": 0, + "path": "live", + "year_over_year_types": 0 + } + }, + "cache.summaries_slice.outcome": "miss", + "correlations": { + "bpCompliance": "insufficient", + "moodPulse": "insufficient", + "weightWeekday": "insufficient", + "path": "live", + "window_days": 28, + "degraded": false, + "tz_guard": "near-utc" + }, + "cache.analytics.outcome": "miss", + "cache.analytics.key_hash": 1185879026 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "ensureUserRollupsFresh", + "args": [ + "" + ] + } + ], + "stored": { + "healthScores": [], + "patterns": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/analytics.09-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.09-anonymous.json.snap new file mode 100644 index 000000000..82d7dcf48 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/analytics.09-anonymous.json.snap @@ -0,0 +1,51 @@ +{ + "status": 401, + "cacheControl": null, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [], + "stored": { + "healthScores": [ + { + "dayKey": "day+0", + "timezone": "UTC", + "composite": 86, + "band": "green", + "scoreVersion": 4, + "composition": [ + "BLOOD_PRESSURE", + "GLYCAEMIA", + "ACTIVITY", + "SLEEP" + ], + "pillarScores": { + "SLEEP": 92, + "ACTIVITY": 78, + "GLYCAEMIA": 100, + "BLOOD_PRESSURE": 73 + }, + "inputFingerprint": "5653cd8c051404182d6c6f813a48b1c2ebc3abe1116901466ab7cc1ab70cb276", + "computedAt": "anchor+0d+12:00:00" + } + ], + "patterns": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/dashboard-summary.01-cold.json.snap b/tests/integration/routes/parity/__snapshots__/dashboard-summary.01-cold.json.snap new file mode 100644 index 000000000..8cc97d561 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/dashboard-summary.01-cold.json.snap @@ -0,0 +1,383 @@ +{ + "status": 200, + "body": { + "data": { + "greeting": { + "salutation": "Hi, Sam", + "date": "anchor+0d+12:00:00" + }, + "streak": { + "currentDays": 120, + "longest": 120, + "label": "days in a row" + }, + "compliance": { + "scheduledToday": 2, + "takenToday": 0 + }, + "highlightInsight": null, + "metrics": [ + { + "id": "weight", + "kind": "weight", + "titleKey": "dashboard.metric.title.weight", + "latestValue": 82, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.weight", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 82.2, + 82.2, + 82.2, + 82.1, + 82.1, + 82, + 82 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+06:30:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "bp", + "kind": "bloodPressure", + "titleKey": "dashboard.metric.title.bloodPressure", + "latestValue": 123, + "secondaryValue": 78, + "unitKey": "dashboard.metric.unit.bloodPressure", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 135, + 128, + 136, + 129, + 137, + 130, + 123 + ], + "updatedAt": "", + "allTimeCount": 182, + "lastSeenAt": "anchor+0d+07:00:00", + "direction": { + "direction": "up-bad", + "targetPosition": "above", + "basis": "profile_default" + }, + "trendSentiment": "positive" + }, + { + "id": "pulse", + "kind": "pulse", + "titleKey": "dashboard.metric.title.pulse", + "latestValue": 64, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.pulse", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 64, + 69, + 68, + 67, + 66, + 65, + 64 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+07:01:00", + "direction": { + "direction": "hold", + "targetPosition": "inside", + "basis": "profile_default" + }, + "trendSentiment": "positive" + }, + { + "id": "bodyFat", + "kind": "bodyFat", + "titleKey": "dashboard.metric.title.bodyFat", + "latestValue": 24, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.bodyFat", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 24.8, + 24.7, + 24.6, + 24.4, + 24.3, + 24.1, + 24 + ], + "updatedAt": "", + "allTimeCount": 9, + "lastSeenAt": "anchor+0d+06:31:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "glucose", + "kind": "glucose", + "titleKey": "dashboard.metric.title.glucose", + "latestValue": 128, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.glucose", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 117, + 115.33333333333333, + 113.66666666666667, + 113.33333333333333, + 111.66666666666667, + 110, + 108.33333333333333 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+11:00:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "positive" + }, + { + "id": "sleep", + "kind": "sleep", + "titleKey": "dashboard.metric.title.sleep", + "latestValue": 5.92, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.sleep", + "unit": "h", + "sleepStages": { + "CORE": 3.33, + "DEEP": 1.17, + "REM": 1.42, + "AWAKE": 0.2 + }, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 6.25, + 6.17, + 6.67, + 6.67, + 7.17, + 6.25, + 5.92 + ], + "updatedAt": "", + "allTimeCount": 85, + "lastSeenAt": "anchor+0d+04:15:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "steps", + "kind": "steps", + "titleKey": "dashboard.metric.title.steps", + "latestValue": 6000, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.steps", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 8238, + 7865, + 7492, + 7119, + 6746, + 6373, + 6000 + ], + "updatedAt": "", + "allTimeCount": 30, + "lastSeenAt": "anchor+0d+09:00:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "negative" + }, + { + "id": "oxygenSaturation", + "kind": "oxygenSaturation", + "titleKey": "dashboard.metric.title.oxygenSaturation", + "latestValue": 96, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.oxygenSaturation", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 96, + 98, + 97, + 96, + 98, + 97, + 96 + ], + "updatedAt": "", + "allTimeCount": 10, + "lastSeenAt": "anchor+0d+04:40:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "mood", + "kind": "mood", + "titleKey": "dashboard.metric.title.mood", + "latestValue": 1, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.mood", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 4, + 1, + 3, + 5, + 2, + 4, + 1 + ], + "updatedAt": "", + "allTimeCount": 45, + "lastSeenAt": "anchor+0d+00:00:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "negative" + }, + { + "id": "bmi", + "kind": "bmi", + "titleKey": "dashboard.metric.title.bmi", + "latestValue": 25.9, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.bmi", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 25.9, + 25.9, + 25.9, + 25.9, + 25.9, + 25.9, + 25.9 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+06:30:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + } + ], + "sleepRhythm": { + "sleepDebt": { + "state": "ready", + "debtMinutes": 145, + "needMinutes": 420, + "nightsCounted": 5, + "windowNights": 5, + "nightsUntilReady": 0, + "source": "COMPUTED" + }, + "chronotype": { + "state": "ready", + "msfMinutes": 63.33521977432794, + "msfScMinutes": 63.335219774327925, + "band": "extreme_early", + "socialJetlagMinutes": 2.402527629787599, + "freeNightsCounted": 6, + "workNightsCounted": 15, + "freeNightsUntilReady": 0 + }, + "averagePerNight": { + "state": "ready", + "averageMinutes": 388, + "nightsCounted": 21, + "nightsUntilReady": 0 + } + }, + "lastUpdated": "anchor+0d+12:00:00" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "dashboard.summary" + }, + "meta": { + "cache.analytics.key_hash": 3272136333, + "cache.analytics.outcome": "miss", + "dashboard.sub_allTime_ms": 0, + "dashboard.sub_bmi_ms": 0, + "dashboard.sub_latestEver_ms": 0, + "dashboard.sub_medsToday_ms": 0, + "dashboard.sub_moodSeries_ms": 0, + "dashboard.sub_sleepNights_ms": 0, + "dashboard.sub_sparkline_ms": 0, + "dashboard.sub_streakDays_ms": 0, + "dashboard.sub_streakIntakes_ms": 0, + "metrics_emitted": 10, + "metrics_gated": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/dashboard-summary.02-warm.json.snap b/tests/integration/routes/parity/__snapshots__/dashboard-summary.02-warm.json.snap new file mode 100644 index 000000000..24b0d1fb1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/dashboard-summary.02-warm.json.snap @@ -0,0 +1,374 @@ +{ + "status": 200, + "body": { + "data": { + "greeting": { + "salutation": "Hi, Sam", + "date": "anchor+0d+12:00:00" + }, + "streak": { + "currentDays": 120, + "longest": 120, + "label": "days in a row" + }, + "compliance": { + "scheduledToday": 2, + "takenToday": 0 + }, + "highlightInsight": null, + "metrics": [ + { + "id": "weight", + "kind": "weight", + "titleKey": "dashboard.metric.title.weight", + "latestValue": 82, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.weight", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 82.2, + 82.2, + 82.2, + 82.1, + 82.1, + 82, + 82 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+06:30:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "bp", + "kind": "bloodPressure", + "titleKey": "dashboard.metric.title.bloodPressure", + "latestValue": 123, + "secondaryValue": 78, + "unitKey": "dashboard.metric.unit.bloodPressure", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 135, + 128, + 136, + 129, + 137, + 130, + 123 + ], + "updatedAt": "", + "allTimeCount": 182, + "lastSeenAt": "anchor+0d+07:00:00", + "direction": { + "direction": "up-bad", + "targetPosition": "above", + "basis": "profile_default" + }, + "trendSentiment": "positive" + }, + { + "id": "pulse", + "kind": "pulse", + "titleKey": "dashboard.metric.title.pulse", + "latestValue": 64, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.pulse", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 64, + 69, + 68, + 67, + 66, + 65, + 64 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+07:01:00", + "direction": { + "direction": "hold", + "targetPosition": "inside", + "basis": "profile_default" + }, + "trendSentiment": "positive" + }, + { + "id": "bodyFat", + "kind": "bodyFat", + "titleKey": "dashboard.metric.title.bodyFat", + "latestValue": 24, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.bodyFat", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 24.8, + 24.7, + 24.6, + 24.4, + 24.3, + 24.1, + 24 + ], + "updatedAt": "", + "allTimeCount": 9, + "lastSeenAt": "anchor+0d+06:31:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "glucose", + "kind": "glucose", + "titleKey": "dashboard.metric.title.glucose", + "latestValue": 128, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.glucose", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 117, + 115.33333333333333, + 113.66666666666667, + 113.33333333333333, + 111.66666666666667, + 110, + 108.33333333333333 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+11:00:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "positive" + }, + { + "id": "sleep", + "kind": "sleep", + "titleKey": "dashboard.metric.title.sleep", + "latestValue": 5.92, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.sleep", + "unit": "h", + "sleepStages": { + "CORE": 3.33, + "DEEP": 1.17, + "REM": 1.42, + "AWAKE": 0.2 + }, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 6.25, + 6.17, + 6.67, + 6.67, + 7.17, + 6.25, + 5.92 + ], + "updatedAt": "", + "allTimeCount": 85, + "lastSeenAt": "anchor+0d+04:15:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "steps", + "kind": "steps", + "titleKey": "dashboard.metric.title.steps", + "latestValue": 6000, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.steps", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 8238, + 7865, + 7492, + 7119, + 6746, + 6373, + 6000 + ], + "updatedAt": "", + "allTimeCount": 30, + "lastSeenAt": "anchor+0d+09:00:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "negative" + }, + { + "id": "oxygenSaturation", + "kind": "oxygenSaturation", + "titleKey": "dashboard.metric.title.oxygenSaturation", + "latestValue": 96, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.oxygenSaturation", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 96, + 98, + 97, + 96, + 98, + 97, + 96 + ], + "updatedAt": "", + "allTimeCount": 10, + "lastSeenAt": "anchor+0d+04:40:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "mood", + "kind": "mood", + "titleKey": "dashboard.metric.title.mood", + "latestValue": 1, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.mood", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 4, + 1, + 3, + 5, + 2, + 4, + 1 + ], + "updatedAt": "", + "allTimeCount": 45, + "lastSeenAt": "anchor+0d+00:00:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "negative" + }, + { + "id": "bmi", + "kind": "bmi", + "titleKey": "dashboard.metric.title.bmi", + "latestValue": 25.9, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.bmi", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 25.9, + 25.9, + 25.9, + 25.9, + 25.9, + 25.9, + 25.9 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+06:30:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + } + ], + "sleepRhythm": { + "sleepDebt": { + "state": "ready", + "debtMinutes": 145, + "needMinutes": 420, + "nightsCounted": 5, + "windowNights": 5, + "nightsUntilReady": 0, + "source": "COMPUTED" + }, + "chronotype": { + "state": "ready", + "msfMinutes": 63.33521977432794, + "msfScMinutes": 63.335219774327925, + "band": "extreme_early", + "socialJetlagMinutes": 2.402527629787599, + "freeNightsCounted": 6, + "workNightsCounted": 15, + "freeNightsUntilReady": 0 + }, + "averagePerNight": { + "state": "ready", + "averageMinutes": 388, + "nightsCounted": 21, + "nightsUntilReady": 0 + } + }, + "lastUpdated": "anchor+0d+12:00:00" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "dashboard.summary" + }, + "meta": { + "cache.analytics.key_hash": 3272136333, + "cache.analytics.outcome": "hit", + "metrics_emitted": 10, + "metrics_gated": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/dashboard-summary.03-modules-off.json.snap b/tests/integration/routes/parity/__snapshots__/dashboard-summary.03-modules-off.json.snap new file mode 100644 index 000000000..59a30ed08 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/dashboard-summary.03-modules-off.json.snap @@ -0,0 +1,279 @@ +{ + "status": 200, + "body": { + "data": { + "greeting": { + "salutation": "Hi, Sam", + "date": "anchor+0d+12:00:00" + }, + "streak": { + "currentDays": 120, + "longest": 120, + "label": "days in a row" + }, + "compliance": { + "scheduledToday": 2, + "takenToday": 0 + }, + "highlightInsight": null, + "metrics": [ + { + "id": "weight", + "kind": "weight", + "titleKey": "dashboard.metric.title.weight", + "latestValue": 82, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.weight", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 82.2, + 82.2, + 82.2, + 82.1, + 82.1, + 82, + 82 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+06:30:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "bp", + "kind": "bloodPressure", + "titleKey": "dashboard.metric.title.bloodPressure", + "latestValue": 123, + "secondaryValue": 78, + "unitKey": "dashboard.metric.unit.bloodPressure", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 135, + 128, + 136, + 129, + 137, + 130, + 123 + ], + "updatedAt": "", + "allTimeCount": 182, + "lastSeenAt": "anchor+0d+07:00:00", + "direction": { + "direction": "up-bad", + "targetPosition": "above", + "basis": "profile_default" + }, + "trendSentiment": "positive" + }, + { + "id": "pulse", + "kind": "pulse", + "titleKey": "dashboard.metric.title.pulse", + "latestValue": 64, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.pulse", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 64, + 69, + 68, + 67, + 66, + 65, + 64 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+07:01:00", + "direction": { + "direction": "hold", + "targetPosition": "inside", + "basis": "profile_default" + }, + "trendSentiment": "positive" + }, + { + "id": "bodyFat", + "kind": "bodyFat", + "titleKey": "dashboard.metric.title.bodyFat", + "latestValue": 24, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.bodyFat", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 24.8, + 24.7, + 24.6, + 24.4, + 24.3, + 24.1, + 24 + ], + "updatedAt": "", + "allTimeCount": 9, + "lastSeenAt": "anchor+0d+06:31:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "steps", + "kind": "steps", + "titleKey": "dashboard.metric.title.steps", + "latestValue": 6000, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.steps", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 8238, + 7865, + 7492, + 7119, + 6746, + 6373, + 6000 + ], + "updatedAt": "", + "allTimeCount": 30, + "lastSeenAt": "anchor+0d+09:00:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "negative" + }, + { + "id": "oxygenSaturation", + "kind": "oxygenSaturation", + "titleKey": "dashboard.metric.title.oxygenSaturation", + "latestValue": 96, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.oxygenSaturation", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 96, + 98, + 97, + 96, + 98, + 97, + 96 + ], + "updatedAt": "", + "allTimeCount": 10, + "lastSeenAt": "anchor+0d+04:40:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "bmi", + "kind": "bmi", + "titleKey": "dashboard.metric.title.bmi", + "latestValue": 25.9, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.bmi", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 25.9, + 25.9, + 25.9, + 25.9, + 25.9, + 25.9, + 25.9 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+06:30:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + } + ], + "sleepRhythm": { + "sleepDebt": { + "state": "ready", + "debtMinutes": 145, + "needMinutes": 420, + "nightsCounted": 5, + "windowNights": 5, + "nightsUntilReady": 0, + "source": "COMPUTED" + }, + "chronotype": { + "state": "ready", + "msfMinutes": 63.33521977432794, + "msfScMinutes": 63.335219774327925, + "band": "extreme_early", + "socialJetlagMinutes": 2.402527629787599, + "freeNightsCounted": 6, + "workNightsCounted": 15, + "freeNightsUntilReady": 0 + }, + "averagePerNight": { + "state": "ready", + "averageMinutes": 388, + "nightsCounted": 21, + "nightsUntilReady": 0 + } + }, + "lastUpdated": "anchor+0d+12:00:00" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "dashboard.summary" + }, + "meta": { + "cache.analytics.key_hash": 3272136333, + "cache.analytics.outcome": "hit", + "metrics_emitted": 7, + "metrics_gated": 3 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/dashboard-summary.04-de-cached.json.snap b/tests/integration/routes/parity/__snapshots__/dashboard-summary.04-de-cached.json.snap new file mode 100644 index 000000000..24b0d1fb1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/dashboard-summary.04-de-cached.json.snap @@ -0,0 +1,374 @@ +{ + "status": 200, + "body": { + "data": { + "greeting": { + "salutation": "Hi, Sam", + "date": "anchor+0d+12:00:00" + }, + "streak": { + "currentDays": 120, + "longest": 120, + "label": "days in a row" + }, + "compliance": { + "scheduledToday": 2, + "takenToday": 0 + }, + "highlightInsight": null, + "metrics": [ + { + "id": "weight", + "kind": "weight", + "titleKey": "dashboard.metric.title.weight", + "latestValue": 82, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.weight", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 82.2, + 82.2, + 82.2, + 82.1, + 82.1, + 82, + 82 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+06:30:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "bp", + "kind": "bloodPressure", + "titleKey": "dashboard.metric.title.bloodPressure", + "latestValue": 123, + "secondaryValue": 78, + "unitKey": "dashboard.metric.unit.bloodPressure", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 135, + 128, + 136, + 129, + 137, + 130, + 123 + ], + "updatedAt": "", + "allTimeCount": 182, + "lastSeenAt": "anchor+0d+07:00:00", + "direction": { + "direction": "up-bad", + "targetPosition": "above", + "basis": "profile_default" + }, + "trendSentiment": "positive" + }, + { + "id": "pulse", + "kind": "pulse", + "titleKey": "dashboard.metric.title.pulse", + "latestValue": 64, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.pulse", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 64, + 69, + 68, + 67, + 66, + 65, + 64 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+07:01:00", + "direction": { + "direction": "hold", + "targetPosition": "inside", + "basis": "profile_default" + }, + "trendSentiment": "positive" + }, + { + "id": "bodyFat", + "kind": "bodyFat", + "titleKey": "dashboard.metric.title.bodyFat", + "latestValue": 24, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.bodyFat", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 24.8, + 24.7, + 24.6, + 24.4, + 24.3, + 24.1, + 24 + ], + "updatedAt": "", + "allTimeCount": 9, + "lastSeenAt": "anchor+0d+06:31:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "glucose", + "kind": "glucose", + "titleKey": "dashboard.metric.title.glucose", + "latestValue": 128, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.glucose", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 117, + 115.33333333333333, + 113.66666666666667, + 113.33333333333333, + 111.66666666666667, + 110, + 108.33333333333333 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+11:00:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "positive" + }, + { + "id": "sleep", + "kind": "sleep", + "titleKey": "dashboard.metric.title.sleep", + "latestValue": 5.92, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.sleep", + "unit": "h", + "sleepStages": { + "CORE": 3.33, + "DEEP": 1.17, + "REM": 1.42, + "AWAKE": 0.2 + }, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 6.25, + 6.17, + 6.67, + 6.67, + 7.17, + 6.25, + 5.92 + ], + "updatedAt": "", + "allTimeCount": 85, + "lastSeenAt": "anchor+0d+04:15:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "steps", + "kind": "steps", + "titleKey": "dashboard.metric.title.steps", + "latestValue": 6000, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.steps", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 8238, + 7865, + 7492, + 7119, + 6746, + 6373, + 6000 + ], + "updatedAt": "", + "allTimeCount": 30, + "lastSeenAt": "anchor+0d+09:00:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "negative" + }, + { + "id": "oxygenSaturation", + "kind": "oxygenSaturation", + "titleKey": "dashboard.metric.title.oxygenSaturation", + "latestValue": 96, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.oxygenSaturation", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 96, + 98, + 97, + 96, + 98, + 97, + 96 + ], + "updatedAt": "", + "allTimeCount": 10, + "lastSeenAt": "anchor+0d+04:40:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "mood", + "kind": "mood", + "titleKey": "dashboard.metric.title.mood", + "latestValue": 1, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.mood", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 4, + 1, + 3, + 5, + 2, + 4, + 1 + ], + "updatedAt": "", + "allTimeCount": 45, + "lastSeenAt": "anchor+0d+00:00:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "negative" + }, + { + "id": "bmi", + "kind": "bmi", + "titleKey": "dashboard.metric.title.bmi", + "latestValue": 25.9, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.bmi", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 25.9, + 25.9, + 25.9, + 25.9, + 25.9, + 25.9, + 25.9 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+06:30:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + } + ], + "sleepRhythm": { + "sleepDebt": { + "state": "ready", + "debtMinutes": 145, + "needMinutes": 420, + "nightsCounted": 5, + "windowNights": 5, + "nightsUntilReady": 0, + "source": "COMPUTED" + }, + "chronotype": { + "state": "ready", + "msfMinutes": 63.33521977432794, + "msfScMinutes": 63.335219774327925, + "band": "extreme_early", + "socialJetlagMinutes": 2.402527629787599, + "freeNightsCounted": 6, + "workNightsCounted": 15, + "freeNightsUntilReady": 0 + }, + "averagePerNight": { + "state": "ready", + "averageMinutes": 388, + "nightsCounted": 21, + "nightsUntilReady": 0 + } + }, + "lastUpdated": "anchor+0d+12:00:00" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "dashboard.summary" + }, + "meta": { + "cache.analytics.key_hash": 3272136333, + "cache.analytics.outcome": "hit", + "metrics_emitted": 10, + "metrics_gated": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/dashboard-summary.05-manager.json.snap b/tests/integration/routes/parity/__snapshots__/dashboard-summary.05-manager.json.snap new file mode 100644 index 000000000..24b0d1fb1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/dashboard-summary.05-manager.json.snap @@ -0,0 +1,374 @@ +{ + "status": 200, + "body": { + "data": { + "greeting": { + "salutation": "Hi, Sam", + "date": "anchor+0d+12:00:00" + }, + "streak": { + "currentDays": 120, + "longest": 120, + "label": "days in a row" + }, + "compliance": { + "scheduledToday": 2, + "takenToday": 0 + }, + "highlightInsight": null, + "metrics": [ + { + "id": "weight", + "kind": "weight", + "titleKey": "dashboard.metric.title.weight", + "latestValue": 82, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.weight", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 82.2, + 82.2, + 82.2, + 82.1, + 82.1, + 82, + 82 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+06:30:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "bp", + "kind": "bloodPressure", + "titleKey": "dashboard.metric.title.bloodPressure", + "latestValue": 123, + "secondaryValue": 78, + "unitKey": "dashboard.metric.unit.bloodPressure", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 135, + 128, + 136, + 129, + 137, + 130, + 123 + ], + "updatedAt": "", + "allTimeCount": 182, + "lastSeenAt": "anchor+0d+07:00:00", + "direction": { + "direction": "up-bad", + "targetPosition": "above", + "basis": "profile_default" + }, + "trendSentiment": "positive" + }, + { + "id": "pulse", + "kind": "pulse", + "titleKey": "dashboard.metric.title.pulse", + "latestValue": 64, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.pulse", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 64, + 69, + 68, + 67, + 66, + 65, + 64 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+07:01:00", + "direction": { + "direction": "hold", + "targetPosition": "inside", + "basis": "profile_default" + }, + "trendSentiment": "positive" + }, + { + "id": "bodyFat", + "kind": "bodyFat", + "titleKey": "dashboard.metric.title.bodyFat", + "latestValue": 24, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.bodyFat", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 24.8, + 24.7, + 24.6, + 24.4, + 24.3, + 24.1, + 24 + ], + "updatedAt": "", + "allTimeCount": 9, + "lastSeenAt": "anchor+0d+06:31:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "glucose", + "kind": "glucose", + "titleKey": "dashboard.metric.title.glucose", + "latestValue": 128, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.glucose", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 117, + 115.33333333333333, + 113.66666666666667, + 113.33333333333333, + 111.66666666666667, + 110, + 108.33333333333333 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+11:00:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "positive" + }, + { + "id": "sleep", + "kind": "sleep", + "titleKey": "dashboard.metric.title.sleep", + "latestValue": 5.92, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.sleep", + "unit": "h", + "sleepStages": { + "CORE": 3.33, + "DEEP": 1.17, + "REM": 1.42, + "AWAKE": 0.2 + }, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 6.25, + 6.17, + 6.67, + 6.67, + 7.17, + 6.25, + 5.92 + ], + "updatedAt": "", + "allTimeCount": 85, + "lastSeenAt": "anchor+0d+04:15:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "steps", + "kind": "steps", + "titleKey": "dashboard.metric.title.steps", + "latestValue": 6000, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.steps", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 8238, + 7865, + 7492, + 7119, + 6746, + 6373, + 6000 + ], + "updatedAt": "", + "allTimeCount": 30, + "lastSeenAt": "anchor+0d+09:00:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "negative" + }, + { + "id": "oxygenSaturation", + "kind": "oxygenSaturation", + "titleKey": "dashboard.metric.title.oxygenSaturation", + "latestValue": 96, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.oxygenSaturation", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 96, + 98, + 97, + 96, + 98, + 97, + 96 + ], + "updatedAt": "", + "allTimeCount": 10, + "lastSeenAt": "anchor+0d+04:40:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + }, + { + "id": "mood", + "kind": "mood", + "titleKey": "dashboard.metric.title.mood", + "latestValue": 1, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.mood", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "down", + "sparkline": [ + 4, + 1, + 3, + 5, + 2, + 4, + 1 + ], + "updatedAt": "", + "allTimeCount": 45, + "lastSeenAt": "anchor+0d+00:00:00", + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "negative" + }, + { + "id": "bmi", + "kind": "bmi", + "titleKey": "dashboard.metric.title.bmi", + "latestValue": 25.9, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.bmi", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "flat", + "sparkline": [ + 25.9, + 25.9, + 25.9, + 25.9, + 25.9, + 25.9, + 25.9 + ], + "updatedAt": "", + "allTimeCount": 60, + "lastSeenAt": "anchor+0d+06:30:00", + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + } + ], + "sleepRhythm": { + "sleepDebt": { + "state": "ready", + "debtMinutes": 145, + "needMinutes": 420, + "nightsCounted": 5, + "windowNights": 5, + "nightsUntilReady": 0, + "source": "COMPUTED" + }, + "chronotype": { + "state": "ready", + "msfMinutes": 63.33521977432794, + "msfScMinutes": 63.335219774327925, + "band": "extreme_early", + "socialJetlagMinutes": 2.402527629787599, + "freeNightsCounted": 6, + "workNightsCounted": 15, + "freeNightsUntilReady": 0 + }, + "averagePerNight": { + "state": "ready", + "averageMinutes": 388, + "nightsCounted": 21, + "nightsUntilReady": 0 + } + }, + "lastUpdated": "anchor+0d+12:00:00" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "dashboard.summary" + }, + "meta": { + "cache.analytics.key_hash": 3272136333, + "cache.analytics.outcome": "hit", + "metrics_emitted": 10, + "metrics_gated": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/dashboard-summary.06-reader.json.snap b/tests/integration/routes/parity/__snapshots__/dashboard-summary.06-reader.json.snap new file mode 100644 index 000000000..b4e2a65a9 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/dashboard-summary.06-reader.json.snap @@ -0,0 +1,26 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "Account access denied", + "meta": { + "errorCode": "sharing.access.denied", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": null, + "meta": { + "sharing_refusal": "insufficient_access" + }, + "warnings": null, + "error": { + "type": "SharingAccessDeniedError", + "message": "Account access denied" + } + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/dashboard-summary.07-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/dashboard-summary.07-anonymous.json.snap new file mode 100644 index 000000000..b1fdd5326 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/dashboard-summary.07-anonymous.json.snap @@ -0,0 +1,24 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/dashboard-summary.08-empty-account.json.snap b/tests/integration/routes/parity/__snapshots__/dashboard-summary.08-empty-account.json.snap new file mode 100644 index 000000000..b71dc8fc2 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/dashboard-summary.08-empty-account.json.snap @@ -0,0 +1,100 @@ +{ + "status": 200, + "body": { + "data": { + "greeting": { + "salutation": "Hi, empty-account", + "date": "anchor+0d+12:00:00" + }, + "streak": { + "currentDays": 0, + "longest": 0, + "label": "days in a row" + }, + "compliance": { + "scheduledToday": 0, + "takenToday": 0 + }, + "highlightInsight": null, + "metrics": [ + { + "id": "weight", + "kind": "weight", + "titleKey": "dashboard.metric.title.weight", + "latestValue": null, + "secondaryValue": null, + "unitKey": "dashboard.metric.unit.weight", + "unit": null, + "sleepStages": null, + "sleepSourceDiscrepancy": null, + "trend": "unknown", + "sparkline": [], + "updatedAt": null, + "allTimeCount": 0, + "lastSeenAt": null, + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "trendSentiment": "neutral" + } + ], + "sleepRhythm": { + "sleepDebt": { + "state": "partial", + "debtMinutes": 0, + "needMinutes": 420, + "nightsCounted": 0, + "windowNights": 5, + "nightsUntilReady": 4, + "source": "COMPUTED" + }, + "chronotype": { + "state": "learning", + "msfMinutes": null, + "msfScMinutes": null, + "band": null, + "socialJetlagMinutes": null, + "freeNightsCounted": 0, + "workNightsCounted": 0, + "freeNightsUntilReady": 3 + }, + "averagePerNight": { + "state": "partial", + "averageMinutes": 0, + "nightsCounted": 0, + "nightsUntilReady": 4 + } + }, + "lastUpdated": "anchor+0d+12:00:00" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "dashboard.summary" + }, + "meta": { + "cache.analytics.key_hash": 1944272078, + "cache.analytics.outcome": "miss", + "dashboard.sub_allTime_ms": 0, + "dashboard.sub_bmi_ms": 0, + "dashboard.sub_latestEver_ms": 0, + "dashboard.sub_medsToday_ms": 0, + "dashboard.sub_moodSeries_ms": 0, + "dashboard.sub_sleepNights_ms": 0, + "dashboard.sub_sparkline_ms": 0, + "dashboard.sub_streakDays_ms": 0, + "dashboard.sub_streakIntakes_ms": 0, + "metrics_emitted": 1, + "metrics_gated": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.get-01-default.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-01-default.json.snap new file mode 100644 index 000000000..4e510a638 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-01-default.json.snap @@ -0,0 +1,86 @@ +{ + "status": 200, + "body": { + "data": { + "documents": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2026-10-02", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [ + { + "episodeId": "", + "name": "Knee" + } + ], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2025-03-04", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "nextCursor": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "documents.inbound.list" + }, + "meta": { + "count": 2, + "sort": "documentDate", + "order": "desc", + "filtered": false + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.get-02-search.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-02-search.json.snap new file mode 100644 index 000000000..b4df20193 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-02-search.json.snap @@ -0,0 +1,54 @@ +{ + "status": 200, + "body": { + "data": { + "documents": [ + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2025-03-04", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "nextCursor": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "documents.inbound.list" + }, + "meta": { + "count": 1, + "sort": "documentDate", + "order": "desc", + "filtered": true + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.get-03-kind.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-03-kind.json.snap new file mode 100644 index 000000000..f562be3b7 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-03-kind.json.snap @@ -0,0 +1,86 @@ +{ + "status": 200, + "body": { + "data": { + "documents": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2026-10-02", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [ + { + "episodeId": "", + "name": "Knee" + } + ], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2025-03-04", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "nextCursor": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "documents.inbound.list" + }, + "meta": { + "count": 2, + "sort": "documentDate", + "order": "desc", + "filtered": true + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.get-04-episode.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-04-episode.json.snap new file mode 100644 index 000000000..4433cdfff --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-04-episode.json.snap @@ -0,0 +1,59 @@ +{ + "status": 200, + "body": { + "data": { + "documents": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2026-10-02", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [ + { + "episodeId": "", + "name": "Knee" + } + ], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "nextCursor": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "documents.inbound.list" + }, + "meta": { + "count": 1, + "sort": "documentDate", + "order": "desc", + "filtered": true + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.get-05-year.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-05-year.json.snap new file mode 100644 index 000000000..b4df20193 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-05-year.json.snap @@ -0,0 +1,54 @@ +{ + "status": 200, + "body": { + "data": { + "documents": [ + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2025-03-04", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "nextCursor": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "documents.inbound.list" + }, + "meta": { + "count": 1, + "sort": "documentDate", + "order": "desc", + "filtered": true + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.get-06-range.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-06-range.json.snap new file mode 100644 index 000000000..0f3ac5e04 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-06-range.json.snap @@ -0,0 +1,59 @@ +{ + "status": 200, + "body": { + "data": { + "documents": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2026-10-02", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [ + { + "episodeId": "", + "name": "Knee" + } + ], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "nextCursor": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "documents.inbound.list" + }, + "meta": { + "count": 1, + "sort": "documentDate", + "order": "desc", + "filtered": false + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.get-07-sorted.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-07-sorted.json.snap new file mode 100644 index 000000000..80854ea45 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-07-sorted.json.snap @@ -0,0 +1,54 @@ +{ + "status": 200, + "body": { + "data": { + "documents": [ + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2025-03-04", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "nextCursor": "" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "documents.inbound.list" + }, + "meta": { + "count": 1, + "sort": "title", + "order": "asc", + "filtered": false + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.get-08-next-page.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-08-next-page.json.snap new file mode 100644 index 000000000..cd302d295 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-08-next-page.json.snap @@ -0,0 +1,59 @@ +{ + "status": 200, + "body": { + "data": { + "documents": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2026-10-02", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [ + { + "episodeId": "", + "name": "Knee" + } + ], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "nextCursor": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "documents.inbound.list" + }, + "meta": { + "count": 1, + "sort": "title", + "order": "asc", + "filtered": false + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.get-09-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-09-invalid.json.snap new file mode 100644 index 000000000..e7205ca8b --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-09-invalid.json.snap @@ -0,0 +1,29 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Invalid list query", + "details": { + "issues": [ + { + "path": "sort", + "code": "invalid_value", + "message": "Invalid option: expected one of \"documentDate\"|\"createdAt\"|\"title\"" + } + ] + }, + "meta": { + "errorCode": "documents.inbound.invalidQuery", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.get-10-delegated.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-10-delegated.json.snap new file mode 100644 index 000000000..4e510a638 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.get-10-delegated.json.snap @@ -0,0 +1,86 @@ +{ + "status": 200, + "body": { + "data": { + "documents": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2026-10-02", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [ + { + "episodeId": "", + "name": "Knee" + } + ], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2025-03-04", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "nextCursor": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "documents.inbound.list" + }, + "meta": { + "count": 2, + "sort": "documentDate", + "order": "desc", + "filtered": false + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-01-stored.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-01-stored.json.snap new file mode 100644 index 000000000..283688949 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-01-stored.json.snap @@ -0,0 +1,95 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2026-10-02", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [ + { + "episodeId": "", + "name": "Knee" + } + ], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "documents.summary.enqueueSkipped" + }, + "meta": { + "documentId": "", + "byteSize": 47, + "servingClass": "inline", + "linked": 1, + "linkedVisits": 0, + "scoped": false, + "sourceSystem": null, + "aiDeferred": false, + "reason": "no_provider" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueDocumentIndex", + "args": [ + "", + "" + ] + }, + { + "effect": "enqueueDocumentThumbnail", + "args": [ + "", + "" + ] + } + ], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-02-duplicate.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-02-duplicate.json.snap new file mode 100644 index 000000000..9e810fbdc --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-02-duplicate.json.snap @@ -0,0 +1,79 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2026-10-02", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [ + { + "episodeId": "", + "name": "Knee" + } + ], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + }, + "error": null, + "meta": { + "duplicate": true + } + }, + "events": [ + { + "status": 200, + "action": { + "name": "documents.vault.upload" + }, + "meta": { + "documentId": "", + "duplicate": true, + "scoped": false, + "sourceSystem": null, + "aiDeferred": false + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-03-second.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-03-second.json.snap new file mode 100644 index 000000000..10cd57835 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-03-second.json.snap @@ -0,0 +1,104 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "providerType": null, + "reportDate": null, + "documentDate": "2025-03-04", + "errorReason": null, + "factCount": 0, + "pendingCount": 0, + "conditionLinks": [], + "encounterLinks": [], + "servingClass": "inline", + "hasContentIndex": false, + "contentIndexSource": null, + "lastIndexAttemptAt": null, + "lastIndexOutcome": null, + "hasThumbnail": false, + "sourceSystem": null, + "sourceId": null, + "createdAt": "", + "updatedAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "documents.summary.enqueueSkipped" + }, + "meta": { + "documentId": "", + "byteSize": 42, + "servingClass": "inline", + "linked": 0, + "linkedVisits": 0, + "scoped": false, + "sourceSystem": null, + "aiDeferred": false, + "reason": "no_provider" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueDocumentIndex", + "args": [ + "", + "" + ] + }, + { + "effect": "enqueueDocumentThumbnail", + "args": [ + "", + "" + ] + } + ], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "documentDate": "2025-03-04T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-04-no-file.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-04-no-file.json.snap new file mode 100644 index 000000000..b59bda209 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-04-no-file.json.snap @@ -0,0 +1,55 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Field 'file' must be a file", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "documentDate": "2025-03-04T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-05-invalid-metadata.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-05-invalid-metadata.json.snap new file mode 100644 index 000000000..487c2088e --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-05-invalid-metadata.json.snap @@ -0,0 +1,64 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Invalid document metadata", + "details": { + "issues": [ + { + "path": "kind", + "code": "invalid_value", + "message": "Invalid option: expected one of \"DOCTOR_REPORT\"|\"DISCHARGE_LETTER\"|\"LAB_RESULT\"|\"IMAGING\"|\"PRESCRIPTION\"|\"REFERRAL\"|\"INSURANCE\"|\"VACCINATION\"|\"SICK_NOTE\"|\"OTHER\"" + } + ] + }, + "meta": { + "errorCode": "documents.inbound.invalidMetadata", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "documentDate": "2025-03-04T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-06-empty.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-06-empty.json.snap new file mode 100644 index 000000000..289166a8c --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-06-empty.json.snap @@ -0,0 +1,55 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Uploaded file is empty", + "meta": { + "errorCode": "documents.inbound.invalidMetadata", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "documentDate": "2025-03-04T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-07-unsupported.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-07-unsupported.json.snap new file mode 100644 index 000000000..573253337 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-07-unsupported.json.snap @@ -0,0 +1,56 @@ +{ + "status": 415, + "body": { + "data": null, + "error": "This file type is not supported.", + "meta": { + "errorCode": "documents.inbound.fileType", + "reason": "unsupportedType", + "requestId": "" + } + }, + "events": [ + { + "status": 415, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "documentDate": "2025-03-04T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-08-unknown-episode.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-08-unknown-episode.json.snap new file mode 100644 index 000000000..9be010e54 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-08-unknown-episode.json.snap @@ -0,0 +1,55 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Episode not found", + "meta": { + "errorCode": "documents.inbound.episodeNotFound", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "documentDate": "2025-03-04T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-09-half-key.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-09-half-key.json.snap new file mode 100644 index 000000000..72f370f5d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-09-half-key.json.snap @@ -0,0 +1,64 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Invalid source key", + "details": { + "issues": [ + { + "path": "sourceId", + "code": "invalid_type", + "message": "Invalid input: expected string, received undefined" + } + ] + }, + "meta": { + "errorCode": "documents.inbound.invalidMetadata", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "documentDate": "2025-03-04T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-10-keys-differ.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-10-keys-differ.json.snap new file mode 100644 index 000000000..8414003ac --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-10-keys-differ.json.snap @@ -0,0 +1,55 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "The source key in the address and in the form differ.", + "meta": { + "errorCode": "documents.inbound.invalidMetadata", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "documentDate": "2025-03-04T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-11-token-stored.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-11-token-stored.json.snap new file mode 100644 index 000000000..d0af23f8a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-11-token-stored.json.snap @@ -0,0 +1,98 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "duplicate": false + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "documents.summary.enqueueSkipped" + }, + "meta": { + "documentId": "", + "byteSize": 46, + "servingClass": "inline", + "linked": 0, + "linkedVisits": 0, + "scoped": true, + "sourceSystem": "PAPERLESS", + "aiDeferred": true, + "reason": "deferred" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueDocumentIndex", + "args": [ + "", + "", + { + "localOnly": true + } + ] + }, + { + "effect": "enqueueDocumentThumbnail", + "args": [ + "", + "" + ] + } + ], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + }, + { + "id": "", + "kind": "OTHER", + "title": null, + "filename": "imported.pdf", + "mimeType": "application/pdf", + "byteSize": 46, + "status": "STORED", + "documentDate": "", + "sourceSystem": "PAPERLESS", + "sourceId": "41", + "deletedAt": null, + "conditionLinks": [] + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "documentDate": "2025-03-04T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-12-token-query-key.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-12-token-query-key.json.snap new file mode 100644 index 000000000..e09fd2747 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-12-token-query-key.json.snap @@ -0,0 +1,79 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "duplicate": true + }, + "error": null, + "meta": { + "duplicate": true + } + }, + "events": [ + { + "status": 200, + "action": { + "name": "documents.vault.upload" + }, + "meta": { + "documentId": "", + "duplicate": true, + "scoped": true, + "sourceSystem": "PAPERLESS", + "aiDeferred": false + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + }, + { + "id": "", + "kind": "OTHER", + "title": null, + "filename": "imported.pdf", + "mimeType": "application/pdf", + "byteSize": 46, + "status": "STORED", + "documentDate": "", + "sourceSystem": "PAPERLESS", + "sourceId": "41", + "deletedAt": null, + "conditionLinks": [] + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "documentDate": "2025-03-04T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-13-token-deleted.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-13-token-deleted.json.snap new file mode 100644 index 000000000..5792498db --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-13-token-deleted.json.snap @@ -0,0 +1,82 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "duplicate": true, + "deleted": true + }, + "error": null, + "meta": { + "duplicate": true, + "deleted": true + } + }, + "events": [ + { + "status": 200, + "action": { + "name": "documents.vault.upload" + }, + "meta": { + "documentId": "", + "duplicate": true, + "deleted": true, + "scoped": true, + "sourceSystem": "PAPERLESS", + "aiDeferred": false + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + }, + { + "id": "", + "kind": "OTHER", + "title": null, + "filename": "imported.pdf", + "mimeType": "application/pdf", + "byteSize": 46, + "status": "STORED", + "documentDate": "", + "sourceSystem": "PAPERLESS", + "sourceId": "41", + "deletedAt": "2026-10-14T09:00:00.000Z", + "conditionLinks": [] + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "documentDate": "2025-03-04T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/documents-inbound.post-14-module-off.json.snap b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-14-module-off.json.snap new file mode 100644 index 000000000..8b7405689 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/documents-inbound.post-14-module-off.json.snap @@ -0,0 +1,70 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "Module \"inboundDocuments\" is not enabled", + "meta": { + "errorCode": "module.disabled", + "module": "inboundDocuments", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": [ + { + "id": "", + "kind": "DISCHARGE_LETTER", + "title": "Discharge letter", + "filename": "discharge.pdf", + "mimeType": "application/pdf", + "byteSize": 47, + "status": "STORED", + "documentDate": "2026-10-02T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [ + { + "episodeId": "" + } + ] + }, + { + "id": "", + "kind": "OTHER", + "title": null, + "filename": "imported.pdf", + "mimeType": "application/pdf", + "byteSize": 46, + "status": "STORED", + "documentDate": "", + "sourceSystem": "PAPERLESS", + "sourceId": "41", + "deletedAt": "2026-10-14T09:00:00.000Z", + "conditionLinks": [] + }, + { + "id": "", + "kind": "LAB_RESULT", + "title": "Blood test", + "filename": "labs.pdf", + "mimeType": "application/pdf", + "byteSize": 42, + "status": "STORED", + "documentDate": "2025-03-04T12:00:00.000Z", + "sourceSystem": null, + "sourceId": null, + "deletedAt": null, + "conditionLinks": [] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/import-csv.01-dry-run.json.snap b/tests/integration/routes/parity/__snapshots__/import-csv.01-dry-run.json.snap new file mode 100644 index 000000000..40b84b9e5 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-csv.01-dry-run.json.snap @@ -0,0 +1,70 @@ +{ + "status": 200, + "body": { + "data": { + "inserted": 7, + "updated": 0, + "skipped": 1, + "total": 8, + "dryRun": true, + "rows": [ + { + "line": 2, + "status": "inserted" + }, + { + "line": 3, + "status": "inserted" + }, + { + "line": 4, + "status": "inserted" + }, + { + "line": 5, + "status": "inserted" + }, + { + "line": 6, + "status": "inserted" + }, + { + "line": 7, + "status": "inserted" + }, + { + "line": 8, + "status": "inserted" + }, + { + "line": 9, + "status": "skipped", + "reason": "invalid_value" + } + ] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "import.csv.upload" + }, + "meta": { + "import_csv_inserted": 7, + "import_csv_updated": 0, + "import_csv_skipped": 1, + "import_csv_dry_run": true + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "measurements": [], + "rollups": [], + "audit": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/import-csv.02-first-import.json.snap b/tests/integration/routes/parity/__snapshots__/import-csv.02-first-import.json.snap new file mode 100644 index 000000000..670cc5f08 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-csv.02-first-import.json.snap @@ -0,0 +1,206 @@ +{ + "status": 200, + "body": { + "data": { + "inserted": 5, + "updated": 1, + "skipped": 2, + "total": 8, + "dryRun": false, + "rows": [ + { + "line": 2, + "status": "inserted" + }, + { + "line": 3, + "status": "inserted" + }, + { + "line": 4, + "status": "updated" + }, + { + "line": 5, + "status": "inserted" + }, + { + "line": 6, + "status": "skipped", + "reason": "duplicate" + }, + { + "line": 7, + "status": "inserted" + }, + { + "line": 8, + "status": "inserted" + }, + { + "line": 9, + "status": "skipped", + "reason": "invalid_value" + } + ] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "import.csv.upload" + }, + "meta": { + "import_csv_inserted": 5, + "import_csv_updated": 1, + "import_csv_skipped": 2, + "import_csv_dry_run": false + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "emitDataArrival", + "args": [ + { + "userId": "", + "kind": "weight", + "newestSampleAt": "2026-10-11T06:30:00.000Z", + "insertedCount": 2, + "refId": "", + "source": "csv_import" + } + ] + }, + { + "effect": "maybeEnqueueMorningRefresh", + "args": [ + "", + [ + "2026-10-12T06:00:00.000Z" + ] + ] + }, + { + "effect": "invalidateStatusInsightsForTypes", + "args": [ + "", + [ + "WEIGHT", + "SLEEP_DURATION", + "BLOOD_GLUCOSE", + "PULSE" + ], + { + "source": "manual" + } + ] + } + ], + "stored": { + "measurements": [ + { + "type": "WEIGHT", + "value": 81.4, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-1", + "measuredAt": "2026-10-10T06:30:00.000Z", + "notes": null, + "notesEncrypted": "", + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "WEIGHT", + "value": 82, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-2", + "measuredAt": "2026-10-11T06:30:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "PULSE", + "value": 64, + "unit": "bpm", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-12T07:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "SLEEP_DURATION", + "value": 412, + "unit": "minutes", + "source": "IMPORT", + "externalId": "exp-s-1", + "measuredAt": "2026-10-12T06:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "BLOOD_GLUCOSE", + "value": 104, + "unit": "mg/dL", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-11T08:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": "FASTING", + "deletedAt": null + } + ], + "rollups": [ + { + "type": "WEIGHT", + "bucketStart": "2026-10-10T00:00:00.000Z", + "count": 1, + "mean": 81.4 + }, + { + "type": "WEIGHT", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 1, + "mean": 82 + }, + { + "type": "PULSE", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 64 + }, + { + "type": "SLEEP_DURATION", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 412 + }, + { + "type": "BLOOD_GLUCOSE", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 1, + "mean": 104 + } + ], + "audit": [ + { + "action": "import.csv.upload", + "details": "{\"inserted\":5,\"updated\":1,\"skipped\":2}" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/import-csv.03-same-file.json.snap b/tests/integration/routes/parity/__snapshots__/import-csv.03-same-file.json.snap new file mode 100644 index 000000000..8751f8834 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-csv.03-same-file.json.snap @@ -0,0 +1,196 @@ +{ + "status": 200, + "body": { + "data": { + "inserted": 0, + "updated": 2, + "skipped": 6, + "total": 8, + "dryRun": false, + "rows": [ + { + "line": 2, + "status": "skipped", + "reason": "duplicate" + }, + { + "line": 3, + "status": "updated" + }, + { + "line": 4, + "status": "updated" + }, + { + "line": 5, + "status": "skipped", + "reason": "duplicate" + }, + { + "line": 6, + "status": "skipped", + "reason": "duplicate" + }, + { + "line": 7, + "status": "skipped", + "reason": "duplicate" + }, + { + "line": 8, + "status": "skipped", + "reason": "duplicate" + }, + { + "line": 9, + "status": "skipped", + "reason": "invalid_value" + } + ] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "import.csv.upload" + }, + "meta": { + "import_csv_inserted": 0, + "import_csv_updated": 2, + "import_csv_skipped": 6, + "import_csv_dry_run": false + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "maybeEnqueueMorningRefresh", + "args": [ + "", + [] + ] + }, + { + "effect": "invalidateStatusInsightsForTypes", + "args": [ + "", + [ + "WEIGHT" + ], + { + "source": "manual" + } + ] + } + ], + "stored": { + "measurements": [ + { + "type": "WEIGHT", + "value": 81.4, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-1", + "measuredAt": "2026-10-10T06:30:00.000Z", + "notes": null, + "notesEncrypted": "", + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "WEIGHT", + "value": 82, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-2", + "measuredAt": "2026-10-11T06:30:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "PULSE", + "value": 64, + "unit": "bpm", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-12T07:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "SLEEP_DURATION", + "value": 412, + "unit": "minutes", + "source": "IMPORT", + "externalId": "exp-s-1", + "measuredAt": "2026-10-12T06:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "BLOOD_GLUCOSE", + "value": 104, + "unit": "mg/dL", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-11T08:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": "FASTING", + "deletedAt": null + } + ], + "rollups": [ + { + "type": "WEIGHT", + "bucketStart": "2026-10-10T00:00:00.000Z", + "count": 1, + "mean": 81.4 + }, + { + "type": "WEIGHT", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 1, + "mean": 82 + }, + { + "type": "PULSE", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 64 + }, + { + "type": "SLEEP_DURATION", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 412 + }, + { + "type": "BLOOD_GLUCOSE", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 1, + "mean": 104 + } + ], + "audit": [ + { + "action": "import.csv.upload", + "details": "{\"inserted\":5,\"updated\":1,\"skipped\":2}" + }, + { + "action": "import.csv.upload", + "details": "{\"inserted\":0,\"updated\":2,\"skipped\":6}" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/import-csv.04-changed-file.json.snap b/tests/integration/routes/parity/__snapshots__/import-csv.04-changed-file.json.snap new file mode 100644 index 000000000..374bc45ae --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-csv.04-changed-file.json.snap @@ -0,0 +1,199 @@ +{ + "status": 200, + "body": { + "data": { + "inserted": 1, + "updated": 2, + "skipped": 1, + "total": 4, + "dryRun": false, + "rows": [ + { + "line": 2, + "status": "skipped", + "reason": "duplicate" + }, + { + "line": 3, + "status": "updated" + }, + { + "line": 4, + "status": "updated" + }, + { + "line": 5, + "status": "inserted" + } + ] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "import.csv.upload" + }, + "meta": { + "import_csv_inserted": 1, + "import_csv_updated": 2, + "import_csv_skipped": 1, + "import_csv_dry_run": false + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "maybeEnqueueMorningRefresh", + "args": [ + "", + [] + ] + }, + { + "effect": "invalidateStatusInsightsForTypes", + "args": [ + "", + [ + "WEIGHT", + "SLEEP_DURATION", + "PULSE" + ], + { + "source": "manual" + } + ] + } + ], + "stored": { + "measurements": [ + { + "type": "WEIGHT", + "value": 82, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-2", + "measuredAt": "2026-10-09T06:30:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "WEIGHT", + "value": 81.4, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-1", + "measuredAt": "2026-10-10T06:30:00.000Z", + "notes": null, + "notesEncrypted": "", + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "PULSE", + "value": 64, + "unit": "bpm", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-12T07:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "PULSE", + "value": 66, + "unit": "bpm", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-13T07:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "SLEEP_DURATION", + "value": 430, + "unit": "minutes", + "source": "IMPORT", + "externalId": "exp-s-1", + "measuredAt": "2026-10-12T06:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "BLOOD_GLUCOSE", + "value": 104, + "unit": "mg/dL", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-11T08:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": "FASTING", + "deletedAt": null + } + ], + "rollups": [ + { + "type": "WEIGHT", + "bucketStart": "2026-10-09T00:00:00.000Z", + "count": 1, + "mean": 82 + }, + { + "type": "WEIGHT", + "bucketStart": "2026-10-10T00:00:00.000Z", + "count": 1, + "mean": 81.4 + }, + { + "type": "PULSE", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 64 + }, + { + "type": "PULSE", + "bucketStart": "2026-10-13T00:00:00.000Z", + "count": 1, + "mean": 66 + }, + { + "type": "SLEEP_DURATION", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 430 + }, + { + "type": "BLOOD_GLUCOSE", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 1, + "mean": 104 + } + ], + "audit": [ + { + "action": "import.csv.upload", + "details": "{\"inserted\":5,\"updated\":1,\"skipped\":2}" + }, + { + "action": "import.csv.upload", + "details": "{\"inserted\":0,\"updated\":2,\"skipped\":6}" + }, + { + "action": "import.csv.upload", + "details": "{\"inserted\":1,\"updated\":2,\"skipped\":1}" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/import-csv.05-unknown-parameter.json.snap b/tests/integration/routes/parity/__snapshots__/import-csv.05-unknown-parameter.json.snap new file mode 100644 index 000000000..40994371b --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-csv.05-unknown-parameter.json.snap @@ -0,0 +1,163 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Validation failed", + "details": { + "issues": [ + { + "path": "", + "code": "unrecognized_keys", + "message": "Unrecognized key: \"dryrun\"", + "keys": [ + "dryrun" + ] + } + ] + }, + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "import.csv.upload" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "measurements": [ + { + "type": "WEIGHT", + "value": 82, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-2", + "measuredAt": "2026-10-09T06:30:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "WEIGHT", + "value": 81.4, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-1", + "measuredAt": "2026-10-10T06:30:00.000Z", + "notes": null, + "notesEncrypted": "", + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "PULSE", + "value": 64, + "unit": "bpm", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-12T07:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "PULSE", + "value": 66, + "unit": "bpm", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-13T07:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "SLEEP_DURATION", + "value": 430, + "unit": "minutes", + "source": "IMPORT", + "externalId": "exp-s-1", + "measuredAt": "2026-10-12T06:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "BLOOD_GLUCOSE", + "value": 104, + "unit": "mg/dL", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-11T08:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": "FASTING", + "deletedAt": null + } + ], + "rollups": [ + { + "type": "WEIGHT", + "bucketStart": "2026-10-09T00:00:00.000Z", + "count": 1, + "mean": 82 + }, + { + "type": "WEIGHT", + "bucketStart": "2026-10-10T00:00:00.000Z", + "count": 1, + "mean": 81.4 + }, + { + "type": "PULSE", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 64 + }, + { + "type": "PULSE", + "bucketStart": "2026-10-13T00:00:00.000Z", + "count": 1, + "mean": 66 + }, + { + "type": "SLEEP_DURATION", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 430 + }, + { + "type": "BLOOD_GLUCOSE", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 1, + "mean": 104 + } + ], + "audit": [ + { + "action": "import.csv.upload", + "details": "{\"inserted\":5,\"updated\":1,\"skipped\":2}" + }, + { + "action": "import.csv.upload", + "details": "{\"inserted\":0,\"updated\":2,\"skipped\":6}" + }, + { + "action": "import.csv.upload", + "details": "{\"inserted\":1,\"updated\":2,\"skipped\":1}" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/import-csv.06-no-header.json.snap b/tests/integration/routes/parity/__snapshots__/import-csv.06-no-header.json.snap new file mode 100644 index 000000000..7bb605762 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-csv.06-no-header.json.snap @@ -0,0 +1,151 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Missing required column(s): type, value, unit, measuredat. Required header: type,value,unit,measuredat[,glucosecontext,notes,externalid].", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "import.csv.upload" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "measurements": [ + { + "type": "WEIGHT", + "value": 82, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-2", + "measuredAt": "2026-10-09T06:30:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "WEIGHT", + "value": 81.4, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-1", + "measuredAt": "2026-10-10T06:30:00.000Z", + "notes": null, + "notesEncrypted": "", + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "PULSE", + "value": 64, + "unit": "bpm", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-12T07:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "PULSE", + "value": 66, + "unit": "bpm", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-13T07:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "SLEEP_DURATION", + "value": 430, + "unit": "minutes", + "source": "IMPORT", + "externalId": "exp-s-1", + "measuredAt": "2026-10-12T06:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null, + "deletedAt": null + }, + { + "type": "BLOOD_GLUCOSE", + "value": 104, + "unit": "mg/dL", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-11T08:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": "FASTING", + "deletedAt": null + } + ], + "rollups": [ + { + "type": "WEIGHT", + "bucketStart": "2026-10-09T00:00:00.000Z", + "count": 1, + "mean": 82 + }, + { + "type": "WEIGHT", + "bucketStart": "2026-10-10T00:00:00.000Z", + "count": 1, + "mean": 81.4 + }, + { + "type": "PULSE", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 64 + }, + { + "type": "PULSE", + "bucketStart": "2026-10-13T00:00:00.000Z", + "count": 1, + "mean": 66 + }, + { + "type": "SLEEP_DURATION", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 430 + }, + { + "type": "BLOOD_GLUCOSE", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 1, + "mean": 104 + } + ], + "audit": [ + { + "action": "import.csv.upload", + "details": "{\"inserted\":5,\"updated\":1,\"skipped\":2}" + }, + { + "action": "import.csv.upload", + "details": "{\"inserted\":0,\"updated\":2,\"skipped\":6}" + }, + { + "action": "import.csv.upload", + "details": "{\"inserted\":1,\"updated\":2,\"skipped\":1}" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/import-json.01-first-file.json.snap b/tests/integration/routes/parity/__snapshots__/import-json.01-first-file.json.snap new file mode 100644 index 000000000..8662cf5e3 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-json.01-first-file.json.snap @@ -0,0 +1,219 @@ +{ + "status": 200, + "body": { + "data": { + "measurements": 4, + "moodEntries": 4, + "skipped": 2 + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "import.upload" + }, + "meta": { + "import_stats": { + "measurements": 4, + "moodEntries": 4, + "skipped": 2 + }, + "external_id_rejected": 2, + "external_id_shapes": "object_description", + "external_id_surface": "import.json" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "emitDataArrival", + "args": [ + { + "userId": "", + "kind": "weight", + "newestSampleAt": "2026-10-11T06:30:00.000Z", + "insertedCount": 2, + "refId": "", + "source": "json_import" + } + ] + }, + { + "effect": "maybeEnqueueMorningRefresh", + "args": [ + "", + [ + "2026-10-12T06:00:00.000Z" + ] + ] + }, + { + "effect": "invalidateStatusInsightsForTypes", + "args": [ + "", + [ + "WEIGHT", + "BLOOD_GLUCOSE", + "SLEEP_DURATION" + ], + { + "source": "manual" + } + ] + } + ], + "stored": { + "measurements": [ + { + "type": "WEIGHT", + "value": 81.4, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-1", + "measuredAt": "2026-10-10T06:30:00.000Z", + "notes": null, + "notesEncrypted": "", + "glucoseContext": null + }, + { + "type": "WEIGHT", + "value": 81.9, + "unit": "kg", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-11T06:30:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + }, + { + "type": "BLOOD_GLUCOSE", + "value": 104, + "unit": "mg/dL", + "source": "IMPORT", + "externalId": "exp-g-1", + "measuredAt": "2026-10-11T08:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": "FASTING" + }, + { + "type": "SLEEP_DURATION", + "value": 412, + "unit": "min", + "source": "IMPORT", + "externalId": "exp-s-1", + "measuredAt": "2026-10-12T06:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + } + ], + "mood": [ + { + "date": "2026-10-10", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "tags": "slept well", + "source": "IMPORT", + "externalId": "daylio-1", + "moodLoggedAt": "2026-10-10T07:00:00.000Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:08.803Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:32.701Z" + }, + { + "date": "2026-10-12", + "mood": "SCHLECHT", + "score": 2, + "moodA1": 3, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-12T20:00:00.000Z" + } + ], + "measurementRollups": [ + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.4 + }, + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.9 + }, + { + "type": "SLEEP_DURATION", + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 412 + }, + { + "type": "BLOOD_GLUCOSE", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 104 + } + ], + "moodRollups": [ + { + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "count": 1, + "mean": 4 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 2, + "mean": 3 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 2 + } + ], + "audit": [ + { + "action": "import.upload", + "details": "{\"measurements\":4,\"moodEntries\":4,\"skipped\":2}" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/import-json.02-same-file.json.snap b/tests/integration/routes/parity/__snapshots__/import-json.02-same-file.json.snap new file mode 100644 index 000000000..845d804ef --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-json.02-same-file.json.snap @@ -0,0 +1,208 @@ +{ + "status": 200, + "body": { + "data": { + "measurements": 3, + "moodEntries": 1, + "skipped": 6 + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "import.upload" + }, + "meta": { + "import_stats": { + "measurements": 3, + "moodEntries": 1, + "skipped": 6 + }, + "external_id_rejected": 2, + "external_id_shapes": "object_description", + "external_id_surface": "import.json" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "maybeEnqueueMorningRefresh", + "args": [ + "", + [] + ] + }, + { + "effect": "invalidateStatusInsightsForTypes", + "args": [ + "", + [ + "WEIGHT", + "BLOOD_GLUCOSE", + "SLEEP_DURATION" + ], + { + "source": "manual" + } + ] + } + ], + "stored": { + "measurements": [ + { + "type": "WEIGHT", + "value": 81.4, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-1", + "measuredAt": "2026-10-10T06:30:00.000Z", + "notes": null, + "notesEncrypted": "", + "glucoseContext": null + }, + { + "type": "WEIGHT", + "value": 81.9, + "unit": "kg", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-11T06:30:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + }, + { + "type": "BLOOD_GLUCOSE", + "value": 104, + "unit": "mg/dL", + "source": "IMPORT", + "externalId": "exp-g-1", + "measuredAt": "2026-10-11T08:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": "FASTING" + }, + { + "type": "SLEEP_DURATION", + "value": 412, + "unit": "min", + "source": "IMPORT", + "externalId": "exp-s-1", + "measuredAt": "2026-10-12T06:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + } + ], + "mood": [ + { + "date": "2026-10-10", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "tags": "slept well", + "source": "IMPORT", + "externalId": "daylio-1", + "moodLoggedAt": "2026-10-10T07:00:00.000Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:08.803Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:32.701Z" + }, + { + "date": "2026-10-12", + "mood": "SCHLECHT", + "score": 2, + "moodA1": 3, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-12T20:00:00.000Z" + } + ], + "measurementRollups": [ + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.4 + }, + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.9 + }, + { + "type": "SLEEP_DURATION", + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 412 + }, + { + "type": "BLOOD_GLUCOSE", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 104 + } + ], + "moodRollups": [ + { + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "count": 1, + "mean": 4 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 2, + "mean": 3 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 2 + } + ], + "audit": [ + { + "action": "import.upload", + "details": "{\"measurements\":4,\"moodEntries\":4,\"skipped\":2}" + }, + { + "action": "import.upload", + "details": "{\"measurements\":3,\"moodEntries\":1,\"skipped\":6}" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/import-json.03-out-of-range.json.snap b/tests/integration/routes/parity/__snapshots__/import-json.03-out-of-range.json.snap new file mode 100644 index 000000000..7e544cc07 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-json.03-out-of-range.json.snap @@ -0,0 +1,186 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Wert ausserhalb des plausiblen Bereichs", + "details": { + "issues": [ + { + "path": "measurements.0", + "code": "custom", + "message": "Wert ausserhalb des plausiblen Bereichs" + } + ] + }, + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "import.upload" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "measurements": [ + { + "type": "WEIGHT", + "value": 81.4, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-1", + "measuredAt": "2026-10-10T06:30:00.000Z", + "notes": null, + "notesEncrypted": "", + "glucoseContext": null + }, + { + "type": "WEIGHT", + "value": 81.9, + "unit": "kg", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-11T06:30:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + }, + { + "type": "BLOOD_GLUCOSE", + "value": 104, + "unit": "mg/dL", + "source": "IMPORT", + "externalId": "exp-g-1", + "measuredAt": "2026-10-11T08:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": "FASTING" + }, + { + "type": "SLEEP_DURATION", + "value": 412, + "unit": "min", + "source": "IMPORT", + "externalId": "exp-s-1", + "measuredAt": "2026-10-12T06:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + } + ], + "mood": [ + { + "date": "2026-10-10", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "tags": "slept well", + "source": "IMPORT", + "externalId": "daylio-1", + "moodLoggedAt": "2026-10-10T07:00:00.000Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:08.803Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:32.701Z" + }, + { + "date": "2026-10-12", + "mood": "SCHLECHT", + "score": 2, + "moodA1": 3, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-12T20:00:00.000Z" + } + ], + "measurementRollups": [ + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.4 + }, + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.9 + }, + { + "type": "SLEEP_DURATION", + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 412 + }, + { + "type": "BLOOD_GLUCOSE", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 104 + } + ], + "moodRollups": [ + { + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "count": 1, + "mean": 4 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 2, + "mean": 3 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 2 + } + ], + "audit": [ + { + "action": "import.upload", + "details": "{\"measurements\":4,\"moodEntries\":4,\"skipped\":2}" + }, + { + "action": "import.upload", + "details": "{\"measurements\":3,\"moodEntries\":1,\"skipped\":6}" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/import-json.04-forward-dated.json.snap b/tests/integration/routes/parity/__snapshots__/import-json.04-forward-dated.json.snap new file mode 100644 index 000000000..bfa999b93 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-json.04-forward-dated.json.snap @@ -0,0 +1,186 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Timestamp must not be in the future", + "details": { + "issues": [ + { + "path": "moodEntries.0.loggedAt", + "code": "custom", + "message": "Timestamp must not be in the future" + } + ] + }, + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "import.upload" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "measurements": [ + { + "type": "WEIGHT", + "value": 81.4, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-1", + "measuredAt": "2026-10-10T06:30:00.000Z", + "notes": null, + "notesEncrypted": "", + "glucoseContext": null + }, + { + "type": "WEIGHT", + "value": 81.9, + "unit": "kg", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-11T06:30:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + }, + { + "type": "BLOOD_GLUCOSE", + "value": 104, + "unit": "mg/dL", + "source": "IMPORT", + "externalId": "exp-g-1", + "measuredAt": "2026-10-11T08:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": "FASTING" + }, + { + "type": "SLEEP_DURATION", + "value": 412, + "unit": "min", + "source": "IMPORT", + "externalId": "exp-s-1", + "measuredAt": "2026-10-12T06:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + } + ], + "mood": [ + { + "date": "2026-10-10", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "tags": "slept well", + "source": "IMPORT", + "externalId": "daylio-1", + "moodLoggedAt": "2026-10-10T07:00:00.000Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:08.803Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:32.701Z" + }, + { + "date": "2026-10-12", + "mood": "SCHLECHT", + "score": 2, + "moodA1": 3, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-12T20:00:00.000Z" + } + ], + "measurementRollups": [ + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.4 + }, + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.9 + }, + { + "type": "SLEEP_DURATION", + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 412 + }, + { + "type": "BLOOD_GLUCOSE", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 104 + } + ], + "moodRollups": [ + { + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "count": 1, + "mean": 4 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 2, + "mean": 3 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 2 + } + ], + "audit": [ + { + "action": "import.upload", + "details": "{\"measurements\":4,\"moodEntries\":4,\"skipped\":2}" + }, + { + "action": "import.upload", + "details": "{\"measurements\":3,\"moodEntries\":1,\"skipped\":6}" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-comprehensive.01-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.01-anonymous.json.snap new file mode 100644 index 000000000..b1fdd5326 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.01-anonymous.json.snap @@ -0,0 +1,24 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-comprehensive.02-cold.json.snap b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.02-cold.json.snap new file mode 100644 index 000000000..44932c375 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.02-cold.json.snap @@ -0,0 +1,1303 @@ +{ + "status": 200, + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 58, + "latest": 82, + "min": 82, + "max": 84.4, + "mean": 83.19, + "median": null, + "avg7": 82.11, + "avg30": 82.59, + "slope7": { + "slope": -0.039, + "direction": "down", + "confidence": 0.89 + }, + "slope30": { + "slope": -0.04, + "direction": "down", + "confidence": 0.99 + }, + "anomalyCount": 0, + "avg30LastMonth": 83.8, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 87, + "latest": 123, + "min": 123, + "max": 137, + "mean": 130.2, + "median": null, + "avg7": 131.14, + "avg30": 130.1, + "slope7": { + "slope": -1.107, + "direction": "down", + "confidence": 0.22 + }, + "slope30": { + "slope": 0.043, + "direction": "up", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 130.24, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 87, + "latest": 78, + "min": 78, + "max": 86, + "mean": 81.94, + "median": null, + "avg7": 81.43, + "avg30": 81.66, + "slope7": { + "slope": -0.5, + "direction": "down", + "confidence": 0.17 + }, + "slope30": { + "slope": -0.019, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": 82.03, + "avg30LastYear": null + }, + "PULSE": { + "count": 58, + "latest": 64, + "min": 64, + "max": 69, + "mean": 66.57, + "median": null, + "avg7": 66.14, + "avg30": 66.55, + "slope7": { + "slope": -0.357, + "direction": "down", + "confidence": 0.16 + }, + "slope30": { + "slope": -0.035, + "direction": "down", + "confidence": 0.03 + }, + "anomalyCount": 0, + "avg30LastMonth": 66.59, + "avg30LastYear": null + }, + "BODY_FAT": { + "count": 8, + "latest": 24, + "min": 24, + "max": 25.1, + "mean": 24.61, + "median": null, + "avg7": 24, + "avg30": 24.33, + "slope7": null, + "slope30": { + "slope": -0.021, + "direction": "down", + "confidence": 0.99 + }, + "anomalyCount": 0, + "avg30LastMonth": 24.9, + "avg30LastYear": null + }, + "SLEEP_DURATION": { + "count": 81, + "latest": 12, + "min": 12, + "max": 240, + "mean": 98.64, + "median": null, + "avg7": 96.17, + "avg30": 98.64, + "slope7": { + "slope": -2.121, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.379, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVITY_STEPS": { + "count": 29, + "latest": 6000, + "min": 6000, + "max": 9833, + "mean": 7780.83, + "median": null, + "avg7": 7119, + "avg30": 7780.83, + "slope7": { + "slope": -373, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -34.049, + "direction": "down", + "confidence": 0.07 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BLOOD_GLUCOSE": { + "count": 57, + "latest": 128, + "min": 92, + "max": 147, + "mean": 113.12, + "median": null, + "avg7": 112.76, + "avg30": 113.12, + "slope7": { + "slope": -1.064, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.113, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "OXYGEN_SATURATION": { + "count": 9, + "latest": 96, + "min": 96, + "max": 98, + "mean": 96.89, + "median": null, + "avg7": 96.86, + "avg30": 96.89, + "slope7": { + "slope": -0.036, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.016, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HEART_RATE_VARIABILITY": { + "count": 29, + "latest": 45, + "min": 45, + "max": 52, + "mean": 48.17, + "median": null, + "avg7": 48, + "avg30": 48.17, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.046, + "direction": "down", + "confidence": 0.03 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESTING_HEART_RATE": { + "count": 38, + "latest": 58, + "min": 58, + "max": 61, + "mean": 59.45, + "median": null, + "avg7": 59.29, + "avg30": 59.38, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.08 + }, + "slope30": { + "slope": -0.009, + "direction": "stable", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 59.67, + "avg30LastYear": null + } + }, + "bmi": 25.9, + "bmiClassification": { + "category": "Overweight", + "color": "var(--warning)", + "severity": "warning" + }, + "bpClassification": { + "category": "High-normal", + "color": "var(--dracula-yellow)", + "severity": "elevated" + }, + "bpPctInTarget": 11, + "bpTargets": { + "sysLow": 120, + "sysHigh": 129, + "diaLow": 70, + "diaHigh": 79 + }, + "weightBpCorrelation": null, + "scatterData": [ + { + "weight": 84.4, + "sysBP": 131 + }, + { + "weight": 84.3, + "sysBP": 124 + }, + { + "weight": 84.3, + "sysBP": 132 + }, + { + "weight": 84.2, + "sysBP": 125 + }, + { + "weight": 84.2, + "sysBP": 133 + }, + { + "weight": 84.2, + "sysBP": 126 + }, + { + "weight": 84.1, + "sysBP": 134 + }, + { + "weight": 84.1, + "sysBP": 127 + }, + { + "weight": 84, + "sysBP": 135 + }, + { + "weight": 84, + "sysBP": 128 + }, + { + "weight": 84, + "sysBP": 136 + }, + { + "weight": 83.9, + "sysBP": 129 + }, + { + "weight": 83.9, + "sysBP": 137 + }, + { + "weight": 83.8, + "sysBP": 130 + }, + { + "weight": 83.8, + "sysBP": 123 + }, + { + "weight": 83.8, + "sysBP": 131 + }, + { + "weight": 83.7, + "sysBP": 124 + }, + { + "weight": 83.7, + "sysBP": 132 + }, + { + "weight": 83.6, + "sysBP": 125 + }, + { + "weight": 83.6, + "sysBP": 133 + }, + { + "weight": 83.6, + "sysBP": 126 + }, + { + "weight": 83.5, + "sysBP": 134 + }, + { + "weight": 83.5, + "sysBP": 127 + }, + { + "weight": 83.4, + "sysBP": 135 + }, + { + "weight": 83.4, + "sysBP": 128 + }, + { + "weight": 83.4, + "sysBP": 136 + }, + { + "weight": 83.3, + "sysBP": 129 + }, + { + "weight": 83.3, + "sysBP": 137 + }, + { + "weight": 83.2, + "sysBP": 130 + }, + { + "weight": 83.2, + "sysBP": 131 + }, + { + "weight": 83.1, + "sysBP": 124 + }, + { + "weight": 83.1, + "sysBP": 132 + }, + { + "weight": 83, + "sysBP": 125 + }, + { + "weight": 83, + "sysBP": 133 + }, + { + "weight": 83, + "sysBP": 126 + }, + { + "weight": 82.9, + "sysBP": 134 + }, + { + "weight": 82.9, + "sysBP": 127 + }, + { + "weight": 82.8, + "sysBP": 135 + }, + { + "weight": 82.8, + "sysBP": 128 + }, + { + "weight": 82.8, + "sysBP": 136 + }, + { + "weight": 82.7, + "sysBP": 129 + }, + { + "weight": 82.7, + "sysBP": 137 + }, + { + "weight": 82.6, + "sysBP": 130 + }, + { + "weight": 82.6, + "sysBP": 123 + }, + { + "weight": 82.6, + "sysBP": 131 + }, + { + "weight": 82.5, + "sysBP": 124 + }, + { + "weight": 82.5, + "sysBP": 132 + }, + { + "weight": 82.4, + "sysBP": 125 + }, + { + "weight": 82.4, + "sysBP": 133 + }, + { + "weight": 82.4, + "sysBP": 126 + }, + { + "weight": 82.3, + "sysBP": 134 + }, + { + "weight": 82.2, + "sysBP": 135 + }, + { + "weight": 82.2, + "sysBP": 128 + }, + { + "weight": 82.2, + "sysBP": 136 + }, + { + "weight": 82.1, + "sysBP": 129 + }, + { + "weight": 82.1, + "sysBP": 137 + }, + { + "weight": 82, + "sysBP": 130 + }, + { + "weight": 82, + "sysBP": 123 + } + ], + "bpMedicationCorrelation": null, + "bpMedicationScatterData": [ + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 124 + }, + { + "continuityPct": 0, + "sysBP": 132 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 126 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 127 + }, + { + "continuityPct": 0, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 129 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + }, + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 0, + "sysBP": 124 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 126 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 127 + }, + { + "continuityPct": 0, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + }, + { + "continuityPct": 100, + "sysBP": 123 + }, + { + "continuityPct": 0, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 124 + }, + { + "continuityPct": 100, + "sysBP": 132 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 129 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + } + ], + "moodSummary": { + "count": 45, + "latest": 1, + "min": 1, + "max": 5, + "mean": 3, + "median": 3, + "avg7": 2.86, + "avg30": 3, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": -0.013, + "direction": "down", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 3, + "avg30LastYear": null + }, + "moodBpCorrelation": null, + "moodBpScatterData": [ + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 2, + "sysBP": 127 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 2, + "sysBP": 127 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 1, + "sysBP": 123 + }, + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 1, + "sysBP": 123 + } + ], + "moodWeightCorrelation": null, + "moodWeightScatterData": [ + { + "mood": 3, + "weight": 83.8 + }, + { + "mood": 5, + "weight": 83.7 + }, + { + "mood": 2, + "weight": 83.7 + }, + { + "mood": 4, + "weight": 83.6 + }, + { + "mood": 1, + "weight": 83.6 + }, + { + "mood": 3, + "weight": 83.6 + }, + { + "mood": 5, + "weight": 83.5 + }, + { + "mood": 2, + "weight": 83.5 + }, + { + "mood": 4, + "weight": 83.4 + }, + { + "mood": 1, + "weight": 83.4 + }, + { + "mood": 3, + "weight": 83.4 + }, + { + "mood": 5, + "weight": 83.3 + }, + { + "mood": 2, + "weight": 83.3 + }, + { + "mood": 4, + "weight": 83.2 + }, + { + "mood": 3, + "weight": 83.2 + }, + { + "mood": 5, + "weight": 83.1 + }, + { + "mood": 2, + "weight": 83.1 + }, + { + "mood": 4, + "weight": 83 + }, + { + "mood": 1, + "weight": 83 + }, + { + "mood": 3, + "weight": 83 + }, + { + "mood": 5, + "weight": 82.9 + }, + { + "mood": 2, + "weight": 82.9 + }, + { + "mood": 4, + "weight": 82.8 + }, + { + "mood": 1, + "weight": 82.8 + }, + { + "mood": 3, + "weight": 82.8 + }, + { + "mood": 5, + "weight": 82.7 + }, + { + "mood": 2, + "weight": 82.7 + }, + { + "mood": 4, + "weight": 82.6 + }, + { + "mood": 1, + "weight": 82.6 + }, + { + "mood": 3, + "weight": 82.6 + }, + { + "mood": 5, + "weight": 82.5 + }, + { + "mood": 2, + "weight": 82.5 + }, + { + "mood": 4, + "weight": 82.4 + }, + { + "mood": 1, + "weight": 82.4 + }, + { + "mood": 3, + "weight": 82.4 + }, + { + "mood": 5, + "weight": 82.3 + }, + { + "mood": 4, + "weight": 82.2 + }, + { + "mood": 1, + "weight": 82.2 + }, + { + "mood": 3, + "weight": 82.2 + }, + { + "mood": 5, + "weight": 82.1 + }, + { + "mood": 2, + "weight": 82.1 + }, + { + "mood": 4, + "weight": 82 + }, + { + "mood": 1, + "weight": 82 + } + ], + "moodPulseCorrelation": null, + "moodPulseScatterData": [ + { + "mood": 3, + "pulse": 61 + }, + { + "mood": 5, + "pulse": 60 + }, + { + "mood": 2, + "pulse": 59 + }, + { + "mood": 4, + "pulse": 58 + }, + { + "mood": 1, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 60 + }, + { + "mood": 5, + "pulse": 59 + }, + { + "mood": 2, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 59 + }, + { + "mood": 5, + "pulse": 58 + }, + { + "mood": 2, + "pulse": 61 + }, + { + "mood": 4, + "pulse": 60 + }, + { + "mood": 1, + "pulse": 59 + }, + { + "mood": 3, + "pulse": 58 + }, + { + "mood": 5, + "pulse": 61 + }, + { + "mood": 2, + "pulse": 60 + }, + { + "mood": 4, + "pulse": 59 + }, + { + "mood": 1, + "pulse": 58 + }, + { + "mood": 3, + "pulse": 61 + }, + { + "mood": 5, + "pulse": 60 + }, + { + "mood": 2, + "pulse": 59 + }, + { + "mood": 4, + "pulse": 58 + }, + { + "mood": 1, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 60 + }, + { + "mood": 5, + "pulse": 59 + }, + { + "mood": 2, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 61 + }, + { + "mood": 1, + "pulse": 60 + }, + { + "mood": 3, + "pulse": 59 + }, + { + "mood": 5, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 60 + }, + { + "mood": 1, + "pulse": 59 + }, + { + "mood": 3, + "pulse": 58 + }, + { + "mood": 5, + "pulse": 61 + }, + { + "mood": 2, + "pulse": 60 + }, + { + "mood": 4, + "pulse": 59 + }, + { + "mood": 1, + "pulse": 58 + } + ], + "medications": [ + { + "id": "", + "name": "Ramipril", + "dose": "5mg", + "category": "BLOOD_PRESSURE", + "categoryLabel": null, + "compliance7": 100, + "compliance30": 76, + "streak": 0, + "taken7": 6, + "skipped7": 0, + "missed7": 0 + }, + { + "id": "", + "name": "Vitamin D", + "dose": "1000 IU", + "category": "OTHER", + "categoryLabel": null, + "compliance7": 43, + "compliance30": 47, + "streak": 1, + "taken7": 3, + "skipped7": 0, + "missed7": 4 + } + ], + "alerts": [ + { + "level": "info", + "title": "BMI in overweight range", + "message": "Your BMI of 25.9 is in the overweight range. Regular exercise and a balanced diet can help." + }, + { + "level": "warning", + "title": "Blood pressure often outside target range", + "message": "Only 11% of your readings are within the target range." + }, + { + "level": "success", + "title": "Excellent compliance: Ramipril", + "message": "100% compliance over the last 7 days. Keep it up!" + }, + { + "level": "warning", + "title": "Low compliance: Vitamin D", + "message": "Your 7-day compliance for Vitamin D is only 43%. Regular intake is important." + } + ], + "hasProvider": true, + "dataSpanDays": 90, + "totalMeasurements": 541, + "revalidating": false, + "ai": { + "briefing": { + "available": true, + "reason": null, + "onDeviceAllowed": true + }, + "statusText": { + "available": true, + "reason": null, + "onDeviceAllowed": true + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.comprehensive" + }, + "meta": { + "cache.analytics.key_hash": 3572442229, + "cache.analytics.outcome": "miss", + "insights": { + "slope_source": "rollup", + "anomaly_source": "live", + "regression_source": "rollup" + }, + "insights.sub_bpRaw_ms": "", + "insights.sub_buckets_ms": "", + "insights.sub_coverage_ms": "", + "insights.sub_firstAt_ms": "", + "insights.sub_latests_ms": "", + "insights.sub_narrow_ms": "", + "medications": 2, + "moodEntries": 45, + "totalMeasurements": 541 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-comprehensive.03-warm.json.snap b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.03-warm.json.snap new file mode 100644 index 000000000..cdd109367 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.03-warm.json.snap @@ -0,0 +1,1274 @@ +{ + "status": 200, + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 58, + "latest": 82, + "min": 82, + "max": 84.4, + "mean": 83.19, + "median": null, + "avg7": 82.11, + "avg30": 82.59, + "slope7": { + "slope": -0.039, + "direction": "down", + "confidence": 0.89 + }, + "slope30": { + "slope": -0.04, + "direction": "down", + "confidence": 0.99 + }, + "anomalyCount": 0, + "avg30LastMonth": 83.8, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 87, + "latest": 123, + "min": 123, + "max": 137, + "mean": 130.2, + "median": null, + "avg7": 131.14, + "avg30": 130.1, + "slope7": { + "slope": -1.107, + "direction": "down", + "confidence": 0.22 + }, + "slope30": { + "slope": 0.043, + "direction": "up", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 130.24, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 87, + "latest": 78, + "min": 78, + "max": 86, + "mean": 81.94, + "median": null, + "avg7": 81.43, + "avg30": 81.66, + "slope7": { + "slope": -0.5, + "direction": "down", + "confidence": 0.17 + }, + "slope30": { + "slope": -0.019, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": 82.03, + "avg30LastYear": null + }, + "PULSE": { + "count": 58, + "latest": 64, + "min": 64, + "max": 69, + "mean": 66.57, + "median": null, + "avg7": 66.14, + "avg30": 66.55, + "slope7": { + "slope": -0.357, + "direction": "down", + "confidence": 0.16 + }, + "slope30": { + "slope": -0.035, + "direction": "down", + "confidence": 0.03 + }, + "anomalyCount": 0, + "avg30LastMonth": 66.59, + "avg30LastYear": null + }, + "BODY_FAT": { + "count": 8, + "latest": 24, + "min": 24, + "max": 25.1, + "mean": 24.61, + "median": null, + "avg7": 24, + "avg30": 24.33, + "slope7": null, + "slope30": { + "slope": -0.021, + "direction": "down", + "confidence": 0.99 + }, + "anomalyCount": 0, + "avg30LastMonth": 24.9, + "avg30LastYear": null + }, + "SLEEP_DURATION": { + "count": 81, + "latest": 12, + "min": 12, + "max": 240, + "mean": 98.64, + "median": null, + "avg7": 96.17, + "avg30": 98.64, + "slope7": { + "slope": -2.121, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.379, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVITY_STEPS": { + "count": 29, + "latest": 6000, + "min": 6000, + "max": 9833, + "mean": 7780.83, + "median": null, + "avg7": 7119, + "avg30": 7780.83, + "slope7": { + "slope": -373, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -34.049, + "direction": "down", + "confidence": 0.07 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BLOOD_GLUCOSE": { + "count": 57, + "latest": 128, + "min": 92, + "max": 147, + "mean": 113.12, + "median": null, + "avg7": 112.76, + "avg30": 113.12, + "slope7": { + "slope": -1.064, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.113, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "OXYGEN_SATURATION": { + "count": 9, + "latest": 96, + "min": 96, + "max": 98, + "mean": 96.89, + "median": null, + "avg7": 96.86, + "avg30": 96.89, + "slope7": { + "slope": -0.036, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.016, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HEART_RATE_VARIABILITY": { + "count": 29, + "latest": 45, + "min": 45, + "max": 52, + "mean": 48.17, + "median": null, + "avg7": 48, + "avg30": 48.17, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.046, + "direction": "down", + "confidence": 0.03 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESTING_HEART_RATE": { + "count": 38, + "latest": 58, + "min": 58, + "max": 61, + "mean": 59.45, + "median": null, + "avg7": 59.29, + "avg30": 59.38, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.08 + }, + "slope30": { + "slope": -0.009, + "direction": "stable", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 59.67, + "avg30LastYear": null + } + }, + "bmi": 25.9, + "bmiClassification": { + "category": "Overweight", + "color": "var(--warning)", + "severity": "warning" + }, + "bpClassification": { + "category": "High-normal", + "color": "var(--dracula-yellow)", + "severity": "elevated" + }, + "bpPctInTarget": 11, + "bpTargets": { + "sysLow": 120, + "sysHigh": 129, + "diaLow": 70, + "diaHigh": 79 + }, + "weightBpCorrelation": null, + "scatterData": [ + { + "weight": 84.4, + "sysBP": 131 + }, + { + "weight": 84.3, + "sysBP": 124 + }, + { + "weight": 84.3, + "sysBP": 132 + }, + { + "weight": 84.2, + "sysBP": 125 + }, + { + "weight": 84.2, + "sysBP": 133 + }, + { + "weight": 84.2, + "sysBP": 126 + }, + { + "weight": 84.1, + "sysBP": 134 + }, + { + "weight": 84.1, + "sysBP": 127 + }, + { + "weight": 84, + "sysBP": 135 + }, + { + "weight": 84, + "sysBP": 128 + }, + { + "weight": 84, + "sysBP": 136 + }, + { + "weight": 83.9, + "sysBP": 129 + }, + { + "weight": 83.9, + "sysBP": 137 + }, + { + "weight": 83.8, + "sysBP": 130 + }, + { + "weight": 83.8, + "sysBP": 123 + }, + { + "weight": 83.8, + "sysBP": 131 + }, + { + "weight": 83.7, + "sysBP": 124 + }, + { + "weight": 83.7, + "sysBP": 132 + }, + { + "weight": 83.6, + "sysBP": 125 + }, + { + "weight": 83.6, + "sysBP": 133 + }, + { + "weight": 83.6, + "sysBP": 126 + }, + { + "weight": 83.5, + "sysBP": 134 + }, + { + "weight": 83.5, + "sysBP": 127 + }, + { + "weight": 83.4, + "sysBP": 135 + }, + { + "weight": 83.4, + "sysBP": 128 + }, + { + "weight": 83.4, + "sysBP": 136 + }, + { + "weight": 83.3, + "sysBP": 129 + }, + { + "weight": 83.3, + "sysBP": 137 + }, + { + "weight": 83.2, + "sysBP": 130 + }, + { + "weight": 83.2, + "sysBP": 131 + }, + { + "weight": 83.1, + "sysBP": 124 + }, + { + "weight": 83.1, + "sysBP": 132 + }, + { + "weight": 83, + "sysBP": 125 + }, + { + "weight": 83, + "sysBP": 133 + }, + { + "weight": 83, + "sysBP": 126 + }, + { + "weight": 82.9, + "sysBP": 134 + }, + { + "weight": 82.9, + "sysBP": 127 + }, + { + "weight": 82.8, + "sysBP": 135 + }, + { + "weight": 82.8, + "sysBP": 128 + }, + { + "weight": 82.8, + "sysBP": 136 + }, + { + "weight": 82.7, + "sysBP": 129 + }, + { + "weight": 82.7, + "sysBP": 137 + }, + { + "weight": 82.6, + "sysBP": 130 + }, + { + "weight": 82.6, + "sysBP": 123 + }, + { + "weight": 82.6, + "sysBP": 131 + }, + { + "weight": 82.5, + "sysBP": 124 + }, + { + "weight": 82.5, + "sysBP": 132 + }, + { + "weight": 82.4, + "sysBP": 125 + }, + { + "weight": 82.4, + "sysBP": 133 + }, + { + "weight": 82.4, + "sysBP": 126 + }, + { + "weight": 82.3, + "sysBP": 134 + }, + { + "weight": 82.2, + "sysBP": 135 + }, + { + "weight": 82.2, + "sysBP": 128 + }, + { + "weight": 82.2, + "sysBP": 136 + }, + { + "weight": 82.1, + "sysBP": 129 + }, + { + "weight": 82.1, + "sysBP": 137 + }, + { + "weight": 82, + "sysBP": 130 + }, + { + "weight": 82, + "sysBP": 123 + } + ], + "bpMedicationCorrelation": null, + "bpMedicationScatterData": [ + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 124 + }, + { + "continuityPct": 0, + "sysBP": 132 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 126 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 127 + }, + { + "continuityPct": 0, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 129 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + }, + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 0, + "sysBP": 124 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 126 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 127 + }, + { + "continuityPct": 0, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + }, + { + "continuityPct": 100, + "sysBP": 123 + }, + { + "continuityPct": 0, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 124 + }, + { + "continuityPct": 100, + "sysBP": 132 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 129 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + } + ], + "moodSummary": { + "count": 45, + "latest": 1, + "min": 1, + "max": 5, + "mean": 3, + "median": 3, + "avg7": 2.86, + "avg30": 3, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": -0.013, + "direction": "down", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 3, + "avg30LastYear": null + }, + "moodBpCorrelation": null, + "moodBpScatterData": [ + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 2, + "sysBP": 127 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 2, + "sysBP": 127 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 1, + "sysBP": 123 + }, + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 1, + "sysBP": 123 + } + ], + "moodWeightCorrelation": null, + "moodWeightScatterData": [ + { + "mood": 3, + "weight": 83.8 + }, + { + "mood": 5, + "weight": 83.7 + }, + { + "mood": 2, + "weight": 83.7 + }, + { + "mood": 4, + "weight": 83.6 + }, + { + "mood": 1, + "weight": 83.6 + }, + { + "mood": 3, + "weight": 83.6 + }, + { + "mood": 5, + "weight": 83.5 + }, + { + "mood": 2, + "weight": 83.5 + }, + { + "mood": 4, + "weight": 83.4 + }, + { + "mood": 1, + "weight": 83.4 + }, + { + "mood": 3, + "weight": 83.4 + }, + { + "mood": 5, + "weight": 83.3 + }, + { + "mood": 2, + "weight": 83.3 + }, + { + "mood": 4, + "weight": 83.2 + }, + { + "mood": 3, + "weight": 83.2 + }, + { + "mood": 5, + "weight": 83.1 + }, + { + "mood": 2, + "weight": 83.1 + }, + { + "mood": 4, + "weight": 83 + }, + { + "mood": 1, + "weight": 83 + }, + { + "mood": 3, + "weight": 83 + }, + { + "mood": 5, + "weight": 82.9 + }, + { + "mood": 2, + "weight": 82.9 + }, + { + "mood": 4, + "weight": 82.8 + }, + { + "mood": 1, + "weight": 82.8 + }, + { + "mood": 3, + "weight": 82.8 + }, + { + "mood": 5, + "weight": 82.7 + }, + { + "mood": 2, + "weight": 82.7 + }, + { + "mood": 4, + "weight": 82.6 + }, + { + "mood": 1, + "weight": 82.6 + }, + { + "mood": 3, + "weight": 82.6 + }, + { + "mood": 5, + "weight": 82.5 + }, + { + "mood": 2, + "weight": 82.5 + }, + { + "mood": 4, + "weight": 82.4 + }, + { + "mood": 1, + "weight": 82.4 + }, + { + "mood": 3, + "weight": 82.4 + }, + { + "mood": 5, + "weight": 82.3 + }, + { + "mood": 4, + "weight": 82.2 + }, + { + "mood": 1, + "weight": 82.2 + }, + { + "mood": 3, + "weight": 82.2 + }, + { + "mood": 5, + "weight": 82.1 + }, + { + "mood": 2, + "weight": 82.1 + }, + { + "mood": 4, + "weight": 82 + }, + { + "mood": 1, + "weight": 82 + } + ], + "moodPulseCorrelation": null, + "moodPulseScatterData": [ + { + "mood": 3, + "pulse": 61 + }, + { + "mood": 5, + "pulse": 60 + }, + { + "mood": 2, + "pulse": 59 + }, + { + "mood": 4, + "pulse": 58 + }, + { + "mood": 1, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 60 + }, + { + "mood": 5, + "pulse": 59 + }, + { + "mood": 2, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 59 + }, + { + "mood": 5, + "pulse": 58 + }, + { + "mood": 2, + "pulse": 61 + }, + { + "mood": 4, + "pulse": 60 + }, + { + "mood": 1, + "pulse": 59 + }, + { + "mood": 3, + "pulse": 58 + }, + { + "mood": 5, + "pulse": 61 + }, + { + "mood": 2, + "pulse": 60 + }, + { + "mood": 4, + "pulse": 59 + }, + { + "mood": 1, + "pulse": 58 + }, + { + "mood": 3, + "pulse": 61 + }, + { + "mood": 5, + "pulse": 60 + }, + { + "mood": 2, + "pulse": 59 + }, + { + "mood": 4, + "pulse": 58 + }, + { + "mood": 1, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 60 + }, + { + "mood": 5, + "pulse": 59 + }, + { + "mood": 2, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 61 + }, + { + "mood": 1, + "pulse": 60 + }, + { + "mood": 3, + "pulse": 59 + }, + { + "mood": 5, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 60 + }, + { + "mood": 1, + "pulse": 59 + }, + { + "mood": 3, + "pulse": 58 + }, + { + "mood": 5, + "pulse": 61 + }, + { + "mood": 2, + "pulse": 60 + }, + { + "mood": 4, + "pulse": 59 + }, + { + "mood": 1, + "pulse": 58 + } + ], + "medications": [ + { + "id": "", + "name": "Ramipril", + "dose": "5mg", + "category": "BLOOD_PRESSURE", + "categoryLabel": null, + "compliance7": 100, + "compliance30": 76, + "streak": 0, + "taken7": 6, + "skipped7": 0, + "missed7": 0 + }, + { + "id": "", + "name": "Vitamin D", + "dose": "1000 IU", + "category": "OTHER", + "categoryLabel": null, + "compliance7": 43, + "compliance30": 47, + "streak": 1, + "taken7": 3, + "skipped7": 0, + "missed7": 4 + } + ], + "alerts": [ + { + "level": "info", + "title": "BMI in overweight range", + "message": "Your BMI of 25.9 is in the overweight range. Regular exercise and a balanced diet can help." + }, + { + "level": "warning", + "title": "Blood pressure often outside target range", + "message": "Only 11% of your readings are within the target range." + }, + { + "level": "success", + "title": "Excellent compliance: Ramipril", + "message": "100% compliance over the last 7 days. Keep it up!" + }, + { + "level": "warning", + "title": "Low compliance: Vitamin D", + "message": "Your 7-day compliance for Vitamin D is only 43%. Regular intake is important." + } + ], + "hasProvider": true, + "dataSpanDays": 90, + "totalMeasurements": 541, + "revalidating": false, + "ai": { + "briefing": { + "available": true, + "reason": null, + "onDeviceAllowed": true + }, + "statusText": { + "available": true, + "reason": null, + "onDeviceAllowed": true + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": null, + "meta": { + "cache.analytics.key_hash": 3572442229, + "cache.analytics.outcome": "hit" + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-comprehensive.04-stale.json.snap b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.04-stale.json.snap new file mode 100644 index 000000000..f2a85fa5b --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.04-stale.json.snap @@ -0,0 +1,1287 @@ +{ + "status": 200, + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 58, + "latest": 82, + "min": 82, + "max": 84.4, + "mean": 83.19, + "median": null, + "avg7": 82.11, + "avg30": 82.59, + "slope7": { + "slope": -0.039, + "direction": "down", + "confidence": 0.89 + }, + "slope30": { + "slope": -0.04, + "direction": "down", + "confidence": 0.99 + }, + "anomalyCount": 0, + "avg30LastMonth": 83.8, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 87, + "latest": 123, + "min": 123, + "max": 137, + "mean": 130.2, + "median": null, + "avg7": 131.14, + "avg30": 130.1, + "slope7": { + "slope": -1.107, + "direction": "down", + "confidence": 0.22 + }, + "slope30": { + "slope": 0.043, + "direction": "up", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 130.24, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 87, + "latest": 78, + "min": 78, + "max": 86, + "mean": 81.94, + "median": null, + "avg7": 81.43, + "avg30": 81.66, + "slope7": { + "slope": -0.5, + "direction": "down", + "confidence": 0.17 + }, + "slope30": { + "slope": -0.019, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": 82.03, + "avg30LastYear": null + }, + "PULSE": { + "count": 58, + "latest": 64, + "min": 64, + "max": 69, + "mean": 66.57, + "median": null, + "avg7": 66.14, + "avg30": 66.55, + "slope7": { + "slope": -0.357, + "direction": "down", + "confidence": 0.16 + }, + "slope30": { + "slope": -0.035, + "direction": "down", + "confidence": 0.03 + }, + "anomalyCount": 0, + "avg30LastMonth": 66.59, + "avg30LastYear": null + }, + "BODY_FAT": { + "count": 8, + "latest": 24, + "min": 24, + "max": 25.1, + "mean": 24.61, + "median": null, + "avg7": 24, + "avg30": 24.33, + "slope7": null, + "slope30": { + "slope": -0.021, + "direction": "down", + "confidence": 0.99 + }, + "anomalyCount": 0, + "avg30LastMonth": 24.9, + "avg30LastYear": null + }, + "SLEEP_DURATION": { + "count": 81, + "latest": 12, + "min": 12, + "max": 240, + "mean": 98.64, + "median": null, + "avg7": 96.17, + "avg30": 98.64, + "slope7": { + "slope": -2.121, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.379, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVITY_STEPS": { + "count": 29, + "latest": 6000, + "min": 6000, + "max": 9833, + "mean": 7780.83, + "median": null, + "avg7": 7119, + "avg30": 7780.83, + "slope7": { + "slope": -373, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -34.049, + "direction": "down", + "confidence": 0.07 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BLOOD_GLUCOSE": { + "count": 57, + "latest": 128, + "min": 92, + "max": 147, + "mean": 113.12, + "median": null, + "avg7": 112.76, + "avg30": 113.12, + "slope7": { + "slope": -1.064, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.113, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "OXYGEN_SATURATION": { + "count": 9, + "latest": 96, + "min": 96, + "max": 98, + "mean": 96.89, + "median": null, + "avg7": 96.86, + "avg30": 96.89, + "slope7": { + "slope": -0.036, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.016, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HEART_RATE_VARIABILITY": { + "count": 29, + "latest": 45, + "min": 45, + "max": 52, + "mean": 48.17, + "median": null, + "avg7": 48, + "avg30": 48.17, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.046, + "direction": "down", + "confidence": 0.03 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESTING_HEART_RATE": { + "count": 38, + "latest": 58, + "min": 58, + "max": 61, + "mean": 59.45, + "median": null, + "avg7": 59.29, + "avg30": 59.38, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.08 + }, + "slope30": { + "slope": -0.009, + "direction": "stable", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 59.67, + "avg30LastYear": null + } + }, + "bmi": 25.9, + "bmiClassification": { + "category": "Overweight", + "color": "var(--warning)", + "severity": "warning" + }, + "bpClassification": { + "category": "High-normal", + "color": "var(--dracula-yellow)", + "severity": "elevated" + }, + "bpPctInTarget": 11, + "bpTargets": { + "sysLow": 120, + "sysHigh": 129, + "diaLow": 70, + "diaHigh": 79 + }, + "weightBpCorrelation": null, + "scatterData": [ + { + "weight": 84.4, + "sysBP": 131 + }, + { + "weight": 84.3, + "sysBP": 124 + }, + { + "weight": 84.3, + "sysBP": 132 + }, + { + "weight": 84.2, + "sysBP": 125 + }, + { + "weight": 84.2, + "sysBP": 133 + }, + { + "weight": 84.2, + "sysBP": 126 + }, + { + "weight": 84.1, + "sysBP": 134 + }, + { + "weight": 84.1, + "sysBP": 127 + }, + { + "weight": 84, + "sysBP": 135 + }, + { + "weight": 84, + "sysBP": 128 + }, + { + "weight": 84, + "sysBP": 136 + }, + { + "weight": 83.9, + "sysBP": 129 + }, + { + "weight": 83.9, + "sysBP": 137 + }, + { + "weight": 83.8, + "sysBP": 130 + }, + { + "weight": 83.8, + "sysBP": 123 + }, + { + "weight": 83.8, + "sysBP": 131 + }, + { + "weight": 83.7, + "sysBP": 124 + }, + { + "weight": 83.7, + "sysBP": 132 + }, + { + "weight": 83.6, + "sysBP": 125 + }, + { + "weight": 83.6, + "sysBP": 133 + }, + { + "weight": 83.6, + "sysBP": 126 + }, + { + "weight": 83.5, + "sysBP": 134 + }, + { + "weight": 83.5, + "sysBP": 127 + }, + { + "weight": 83.4, + "sysBP": 135 + }, + { + "weight": 83.4, + "sysBP": 128 + }, + { + "weight": 83.4, + "sysBP": 136 + }, + { + "weight": 83.3, + "sysBP": 129 + }, + { + "weight": 83.3, + "sysBP": 137 + }, + { + "weight": 83.2, + "sysBP": 130 + }, + { + "weight": 83.2, + "sysBP": 131 + }, + { + "weight": 83.1, + "sysBP": 124 + }, + { + "weight": 83.1, + "sysBP": 132 + }, + { + "weight": 83, + "sysBP": 125 + }, + { + "weight": 83, + "sysBP": 133 + }, + { + "weight": 83, + "sysBP": 126 + }, + { + "weight": 82.9, + "sysBP": 134 + }, + { + "weight": 82.9, + "sysBP": 127 + }, + { + "weight": 82.8, + "sysBP": 135 + }, + { + "weight": 82.8, + "sysBP": 128 + }, + { + "weight": 82.8, + "sysBP": 136 + }, + { + "weight": 82.7, + "sysBP": 129 + }, + { + "weight": 82.7, + "sysBP": 137 + }, + { + "weight": 82.6, + "sysBP": 130 + }, + { + "weight": 82.6, + "sysBP": 123 + }, + { + "weight": 82.6, + "sysBP": 131 + }, + { + "weight": 82.5, + "sysBP": 124 + }, + { + "weight": 82.5, + "sysBP": 132 + }, + { + "weight": 82.4, + "sysBP": 125 + }, + { + "weight": 82.4, + "sysBP": 133 + }, + { + "weight": 82.4, + "sysBP": 126 + }, + { + "weight": 82.3, + "sysBP": 134 + }, + { + "weight": 82.2, + "sysBP": 135 + }, + { + "weight": 82.2, + "sysBP": 128 + }, + { + "weight": 82.2, + "sysBP": 136 + }, + { + "weight": 82.1, + "sysBP": 129 + }, + { + "weight": 82.1, + "sysBP": 137 + }, + { + "weight": 82, + "sysBP": 130 + }, + { + "weight": 82, + "sysBP": 123 + } + ], + "bpMedicationCorrelation": null, + "bpMedicationScatterData": [ + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 124 + }, + { + "continuityPct": 0, + "sysBP": 132 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 126 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 127 + }, + { + "continuityPct": 0, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 129 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + }, + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 0, + "sysBP": 124 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 126 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 127 + }, + { + "continuityPct": 0, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + }, + { + "continuityPct": 100, + "sysBP": 123 + }, + { + "continuityPct": 0, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 124 + }, + { + "continuityPct": 100, + "sysBP": 132 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 129 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + } + ], + "moodSummary": { + "count": 45, + "latest": 1, + "min": 1, + "max": 5, + "mean": 3, + "median": 3, + "avg7": 2.86, + "avg30": 3, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": -0.013, + "direction": "down", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 3, + "avg30LastYear": null + }, + "moodBpCorrelation": null, + "moodBpScatterData": [ + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 2, + "sysBP": 127 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 2, + "sysBP": 127 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 1, + "sysBP": 123 + }, + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 1, + "sysBP": 123 + } + ], + "moodWeightCorrelation": null, + "moodWeightScatterData": [ + { + "mood": 3, + "weight": 83.8 + }, + { + "mood": 5, + "weight": 83.7 + }, + { + "mood": 2, + "weight": 83.7 + }, + { + "mood": 4, + "weight": 83.6 + }, + { + "mood": 1, + "weight": 83.6 + }, + { + "mood": 3, + "weight": 83.6 + }, + { + "mood": 5, + "weight": 83.5 + }, + { + "mood": 2, + "weight": 83.5 + }, + { + "mood": 4, + "weight": 83.4 + }, + { + "mood": 1, + "weight": 83.4 + }, + { + "mood": 3, + "weight": 83.4 + }, + { + "mood": 5, + "weight": 83.3 + }, + { + "mood": 2, + "weight": 83.3 + }, + { + "mood": 4, + "weight": 83.2 + }, + { + "mood": 3, + "weight": 83.2 + }, + { + "mood": 5, + "weight": 83.1 + }, + { + "mood": 2, + "weight": 83.1 + }, + { + "mood": 4, + "weight": 83 + }, + { + "mood": 1, + "weight": 83 + }, + { + "mood": 3, + "weight": 83 + }, + { + "mood": 5, + "weight": 82.9 + }, + { + "mood": 2, + "weight": 82.9 + }, + { + "mood": 4, + "weight": 82.8 + }, + { + "mood": 1, + "weight": 82.8 + }, + { + "mood": 3, + "weight": 82.8 + }, + { + "mood": 5, + "weight": 82.7 + }, + { + "mood": 2, + "weight": 82.7 + }, + { + "mood": 4, + "weight": 82.6 + }, + { + "mood": 1, + "weight": 82.6 + }, + { + "mood": 3, + "weight": 82.6 + }, + { + "mood": 5, + "weight": 82.5 + }, + { + "mood": 2, + "weight": 82.5 + }, + { + "mood": 4, + "weight": 82.4 + }, + { + "mood": 1, + "weight": 82.4 + }, + { + "mood": 3, + "weight": 82.4 + }, + { + "mood": 5, + "weight": 82.3 + }, + { + "mood": 4, + "weight": 82.2 + }, + { + "mood": 1, + "weight": 82.2 + }, + { + "mood": 3, + "weight": 82.2 + }, + { + "mood": 5, + "weight": 82.1 + }, + { + "mood": 2, + "weight": 82.1 + }, + { + "mood": 4, + "weight": 82 + }, + { + "mood": 1, + "weight": 82 + } + ], + "moodPulseCorrelation": null, + "moodPulseScatterData": [ + { + "mood": 3, + "pulse": 61 + }, + { + "mood": 5, + "pulse": 60 + }, + { + "mood": 2, + "pulse": 59 + }, + { + "mood": 4, + "pulse": 58 + }, + { + "mood": 1, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 60 + }, + { + "mood": 5, + "pulse": 59 + }, + { + "mood": 2, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 59 + }, + { + "mood": 5, + "pulse": 58 + }, + { + "mood": 2, + "pulse": 61 + }, + { + "mood": 4, + "pulse": 60 + }, + { + "mood": 1, + "pulse": 59 + }, + { + "mood": 3, + "pulse": 58 + }, + { + "mood": 5, + "pulse": 61 + }, + { + "mood": 2, + "pulse": 60 + }, + { + "mood": 4, + "pulse": 59 + }, + { + "mood": 1, + "pulse": 58 + }, + { + "mood": 3, + "pulse": 61 + }, + { + "mood": 5, + "pulse": 60 + }, + { + "mood": 2, + "pulse": 59 + }, + { + "mood": 4, + "pulse": 58 + }, + { + "mood": 1, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 60 + }, + { + "mood": 5, + "pulse": 59 + }, + { + "mood": 2, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 61 + }, + { + "mood": 1, + "pulse": 60 + }, + { + "mood": 3, + "pulse": 59 + }, + { + "mood": 5, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 60 + }, + { + "mood": 1, + "pulse": 59 + }, + { + "mood": 3, + "pulse": 58 + }, + { + "mood": 5, + "pulse": 61 + }, + { + "mood": 2, + "pulse": 60 + }, + { + "mood": 4, + "pulse": 59 + }, + { + "mood": 1, + "pulse": 58 + } + ], + "medications": [ + { + "id": "", + "name": "Ramipril", + "dose": "5mg", + "category": "BLOOD_PRESSURE", + "categoryLabel": null, + "compliance7": 100, + "compliance30": 76, + "streak": 0, + "taken7": 6, + "skipped7": 0, + "missed7": 0 + }, + { + "id": "", + "name": "Vitamin D", + "dose": "1000 IU", + "category": "OTHER", + "categoryLabel": null, + "compliance7": 43, + "compliance30": 47, + "streak": 1, + "taken7": 3, + "skipped7": 0, + "missed7": 4 + } + ], + "alerts": [ + { + "level": "info", + "title": "BMI in overweight range", + "message": "Your BMI of 25.9 is in the overweight range. Regular exercise and a balanced diet can help." + }, + { + "level": "warning", + "title": "Blood pressure often outside target range", + "message": "Only 11% of your readings are within the target range." + }, + { + "level": "success", + "title": "Excellent compliance: Ramipril", + "message": "100% compliance over the last 7 days. Keep it up!" + }, + { + "level": "warning", + "title": "Low compliance: Vitamin D", + "message": "Your 7-day compliance for Vitamin D is only 43%. Regular intake is important." + } + ], + "hasProvider": true, + "dataSpanDays": 90, + "totalMeasurements": 541, + "revalidating": true, + "ai": { + "briefing": { + "available": true, + "reason": null, + "onDeviceAllowed": true + }, + "statusText": { + "available": true, + "reason": null, + "onDeviceAllowed": true + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": "", + "meta": { + "cache.analytics.key_hash": 3572442229, + "cache.analytics.outcome": "stale" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-comprehensive.05-ai-analysis-off.json.snap b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.05-ai-analysis-off.json.snap new file mode 100644 index 000000000..7235cd05c --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.05-ai-analysis-off.json.snap @@ -0,0 +1,1274 @@ +{ + "status": 200, + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 58, + "latest": 82, + "min": 82, + "max": 84.4, + "mean": 83.19, + "median": null, + "avg7": 82.11, + "avg30": 82.59, + "slope7": { + "slope": -0.039, + "direction": "down", + "confidence": 0.89 + }, + "slope30": { + "slope": -0.04, + "direction": "down", + "confidence": 0.99 + }, + "anomalyCount": 0, + "avg30LastMonth": 83.8, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 87, + "latest": 123, + "min": 123, + "max": 137, + "mean": 130.2, + "median": null, + "avg7": 131.14, + "avg30": 130.1, + "slope7": { + "slope": -1.107, + "direction": "down", + "confidence": 0.22 + }, + "slope30": { + "slope": 0.043, + "direction": "up", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 130.24, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 87, + "latest": 78, + "min": 78, + "max": 86, + "mean": 81.94, + "median": null, + "avg7": 81.43, + "avg30": 81.66, + "slope7": { + "slope": -0.5, + "direction": "down", + "confidence": 0.17 + }, + "slope30": { + "slope": -0.019, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": 82.03, + "avg30LastYear": null + }, + "PULSE": { + "count": 58, + "latest": 64, + "min": 64, + "max": 69, + "mean": 66.57, + "median": null, + "avg7": 66.14, + "avg30": 66.55, + "slope7": { + "slope": -0.357, + "direction": "down", + "confidence": 0.16 + }, + "slope30": { + "slope": -0.035, + "direction": "down", + "confidence": 0.03 + }, + "anomalyCount": 0, + "avg30LastMonth": 66.59, + "avg30LastYear": null + }, + "BODY_FAT": { + "count": 8, + "latest": 24, + "min": 24, + "max": 25.1, + "mean": 24.61, + "median": null, + "avg7": 24, + "avg30": 24.33, + "slope7": null, + "slope30": { + "slope": -0.021, + "direction": "down", + "confidence": 0.99 + }, + "anomalyCount": 0, + "avg30LastMonth": 24.9, + "avg30LastYear": null + }, + "SLEEP_DURATION": { + "count": 81, + "latest": 12, + "min": 12, + "max": 240, + "mean": 98.64, + "median": null, + "avg7": 96.17, + "avg30": 98.64, + "slope7": { + "slope": -2.121, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.379, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVITY_STEPS": { + "count": 29, + "latest": 6000, + "min": 6000, + "max": 9833, + "mean": 7780.83, + "median": null, + "avg7": 7119, + "avg30": 7780.83, + "slope7": { + "slope": -373, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -34.049, + "direction": "down", + "confidence": 0.07 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BLOOD_GLUCOSE": { + "count": 57, + "latest": 128, + "min": 92, + "max": 147, + "mean": 113.12, + "median": null, + "avg7": 112.76, + "avg30": 113.12, + "slope7": { + "slope": -1.064, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.113, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "OXYGEN_SATURATION": { + "count": 9, + "latest": 96, + "min": 96, + "max": 98, + "mean": 96.89, + "median": null, + "avg7": 96.86, + "avg30": 96.89, + "slope7": { + "slope": -0.036, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.016, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HEART_RATE_VARIABILITY": { + "count": 29, + "latest": 45, + "min": 45, + "max": 52, + "mean": 48.17, + "median": null, + "avg7": 48, + "avg30": 48.17, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.046, + "direction": "down", + "confidence": 0.03 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESTING_HEART_RATE": { + "count": 38, + "latest": 58, + "min": 58, + "max": 61, + "mean": 59.45, + "median": null, + "avg7": 59.29, + "avg30": 59.38, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.08 + }, + "slope30": { + "slope": -0.009, + "direction": "stable", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 59.67, + "avg30LastYear": null + } + }, + "bmi": 25.9, + "bmiClassification": { + "category": "Overweight", + "color": "var(--warning)", + "severity": "warning" + }, + "bpClassification": { + "category": "High-normal", + "color": "var(--dracula-yellow)", + "severity": "elevated" + }, + "bpPctInTarget": 11, + "bpTargets": { + "sysLow": 120, + "sysHigh": 129, + "diaLow": 70, + "diaHigh": 79 + }, + "weightBpCorrelation": null, + "scatterData": [ + { + "weight": 84.4, + "sysBP": 131 + }, + { + "weight": 84.3, + "sysBP": 124 + }, + { + "weight": 84.3, + "sysBP": 132 + }, + { + "weight": 84.2, + "sysBP": 125 + }, + { + "weight": 84.2, + "sysBP": 133 + }, + { + "weight": 84.2, + "sysBP": 126 + }, + { + "weight": 84.1, + "sysBP": 134 + }, + { + "weight": 84.1, + "sysBP": 127 + }, + { + "weight": 84, + "sysBP": 135 + }, + { + "weight": 84, + "sysBP": 128 + }, + { + "weight": 84, + "sysBP": 136 + }, + { + "weight": 83.9, + "sysBP": 129 + }, + { + "weight": 83.9, + "sysBP": 137 + }, + { + "weight": 83.8, + "sysBP": 130 + }, + { + "weight": 83.8, + "sysBP": 123 + }, + { + "weight": 83.8, + "sysBP": 131 + }, + { + "weight": 83.7, + "sysBP": 124 + }, + { + "weight": 83.7, + "sysBP": 132 + }, + { + "weight": 83.6, + "sysBP": 125 + }, + { + "weight": 83.6, + "sysBP": 133 + }, + { + "weight": 83.6, + "sysBP": 126 + }, + { + "weight": 83.5, + "sysBP": 134 + }, + { + "weight": 83.5, + "sysBP": 127 + }, + { + "weight": 83.4, + "sysBP": 135 + }, + { + "weight": 83.4, + "sysBP": 128 + }, + { + "weight": 83.4, + "sysBP": 136 + }, + { + "weight": 83.3, + "sysBP": 129 + }, + { + "weight": 83.3, + "sysBP": 137 + }, + { + "weight": 83.2, + "sysBP": 130 + }, + { + "weight": 83.2, + "sysBP": 131 + }, + { + "weight": 83.1, + "sysBP": 124 + }, + { + "weight": 83.1, + "sysBP": 132 + }, + { + "weight": 83, + "sysBP": 125 + }, + { + "weight": 83, + "sysBP": 133 + }, + { + "weight": 83, + "sysBP": 126 + }, + { + "weight": 82.9, + "sysBP": 134 + }, + { + "weight": 82.9, + "sysBP": 127 + }, + { + "weight": 82.8, + "sysBP": 135 + }, + { + "weight": 82.8, + "sysBP": 128 + }, + { + "weight": 82.8, + "sysBP": 136 + }, + { + "weight": 82.7, + "sysBP": 129 + }, + { + "weight": 82.7, + "sysBP": 137 + }, + { + "weight": 82.6, + "sysBP": 130 + }, + { + "weight": 82.6, + "sysBP": 123 + }, + { + "weight": 82.6, + "sysBP": 131 + }, + { + "weight": 82.5, + "sysBP": 124 + }, + { + "weight": 82.5, + "sysBP": 132 + }, + { + "weight": 82.4, + "sysBP": 125 + }, + { + "weight": 82.4, + "sysBP": 133 + }, + { + "weight": 82.4, + "sysBP": 126 + }, + { + "weight": 82.3, + "sysBP": 134 + }, + { + "weight": 82.2, + "sysBP": 135 + }, + { + "weight": 82.2, + "sysBP": 128 + }, + { + "weight": 82.2, + "sysBP": 136 + }, + { + "weight": 82.1, + "sysBP": 129 + }, + { + "weight": 82.1, + "sysBP": 137 + }, + { + "weight": 82, + "sysBP": 130 + }, + { + "weight": 82, + "sysBP": 123 + } + ], + "bpMedicationCorrelation": null, + "bpMedicationScatterData": [ + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 124 + }, + { + "continuityPct": 0, + "sysBP": 132 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 126 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 127 + }, + { + "continuityPct": 0, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 129 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + }, + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 0, + "sysBP": 124 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 126 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 127 + }, + { + "continuityPct": 0, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + }, + { + "continuityPct": 100, + "sysBP": 123 + }, + { + "continuityPct": 0, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 124 + }, + { + "continuityPct": 100, + "sysBP": 132 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 129 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + } + ], + "moodSummary": { + "count": 45, + "latest": 1, + "min": 1, + "max": 5, + "mean": 3, + "median": 3, + "avg7": 2.86, + "avg30": 3, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": -0.013, + "direction": "down", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 3, + "avg30LastYear": null + }, + "moodBpCorrelation": null, + "moodBpScatterData": [ + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 2, + "sysBP": 127 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 2, + "sysBP": 127 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 1, + "sysBP": 123 + }, + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 1, + "sysBP": 123 + } + ], + "moodWeightCorrelation": null, + "moodWeightScatterData": [ + { + "mood": 3, + "weight": 83.8 + }, + { + "mood": 5, + "weight": 83.7 + }, + { + "mood": 2, + "weight": 83.7 + }, + { + "mood": 4, + "weight": 83.6 + }, + { + "mood": 1, + "weight": 83.6 + }, + { + "mood": 3, + "weight": 83.6 + }, + { + "mood": 5, + "weight": 83.5 + }, + { + "mood": 2, + "weight": 83.5 + }, + { + "mood": 4, + "weight": 83.4 + }, + { + "mood": 1, + "weight": 83.4 + }, + { + "mood": 3, + "weight": 83.4 + }, + { + "mood": 5, + "weight": 83.3 + }, + { + "mood": 2, + "weight": 83.3 + }, + { + "mood": 4, + "weight": 83.2 + }, + { + "mood": 3, + "weight": 83.2 + }, + { + "mood": 5, + "weight": 83.1 + }, + { + "mood": 2, + "weight": 83.1 + }, + { + "mood": 4, + "weight": 83 + }, + { + "mood": 1, + "weight": 83 + }, + { + "mood": 3, + "weight": 83 + }, + { + "mood": 5, + "weight": 82.9 + }, + { + "mood": 2, + "weight": 82.9 + }, + { + "mood": 4, + "weight": 82.8 + }, + { + "mood": 1, + "weight": 82.8 + }, + { + "mood": 3, + "weight": 82.8 + }, + { + "mood": 5, + "weight": 82.7 + }, + { + "mood": 2, + "weight": 82.7 + }, + { + "mood": 4, + "weight": 82.6 + }, + { + "mood": 1, + "weight": 82.6 + }, + { + "mood": 3, + "weight": 82.6 + }, + { + "mood": 5, + "weight": 82.5 + }, + { + "mood": 2, + "weight": 82.5 + }, + { + "mood": 4, + "weight": 82.4 + }, + { + "mood": 1, + "weight": 82.4 + }, + { + "mood": 3, + "weight": 82.4 + }, + { + "mood": 5, + "weight": 82.3 + }, + { + "mood": 4, + "weight": 82.2 + }, + { + "mood": 1, + "weight": 82.2 + }, + { + "mood": 3, + "weight": 82.2 + }, + { + "mood": 5, + "weight": 82.1 + }, + { + "mood": 2, + "weight": 82.1 + }, + { + "mood": 4, + "weight": 82 + }, + { + "mood": 1, + "weight": 82 + } + ], + "moodPulseCorrelation": null, + "moodPulseScatterData": [ + { + "mood": 3, + "pulse": 61 + }, + { + "mood": 5, + "pulse": 60 + }, + { + "mood": 2, + "pulse": 59 + }, + { + "mood": 4, + "pulse": 58 + }, + { + "mood": 1, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 60 + }, + { + "mood": 5, + "pulse": 59 + }, + { + "mood": 2, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 59 + }, + { + "mood": 5, + "pulse": 58 + }, + { + "mood": 2, + "pulse": 61 + }, + { + "mood": 4, + "pulse": 60 + }, + { + "mood": 1, + "pulse": 59 + }, + { + "mood": 3, + "pulse": 58 + }, + { + "mood": 5, + "pulse": 61 + }, + { + "mood": 2, + "pulse": 60 + }, + { + "mood": 4, + "pulse": 59 + }, + { + "mood": 1, + "pulse": 58 + }, + { + "mood": 3, + "pulse": 61 + }, + { + "mood": 5, + "pulse": 60 + }, + { + "mood": 2, + "pulse": 59 + }, + { + "mood": 4, + "pulse": 58 + }, + { + "mood": 1, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 60 + }, + { + "mood": 5, + "pulse": 59 + }, + { + "mood": 2, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 61 + }, + { + "mood": 1, + "pulse": 60 + }, + { + "mood": 3, + "pulse": 59 + }, + { + "mood": 5, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 60 + }, + { + "mood": 1, + "pulse": 59 + }, + { + "mood": 3, + "pulse": 58 + }, + { + "mood": 5, + "pulse": 61 + }, + { + "mood": 2, + "pulse": 60 + }, + { + "mood": 4, + "pulse": 59 + }, + { + "mood": 1, + "pulse": 58 + } + ], + "medications": [ + { + "id": "", + "name": "Ramipril", + "dose": "5mg", + "category": "BLOOD_PRESSURE", + "categoryLabel": null, + "compliance7": 100, + "compliance30": 76, + "streak": 0, + "taken7": 6, + "skipped7": 0, + "missed7": 0 + }, + { + "id": "", + "name": "Vitamin D", + "dose": "1000 IU", + "category": "OTHER", + "categoryLabel": null, + "compliance7": 43, + "compliance30": 47, + "streak": 1, + "taken7": 3, + "skipped7": 0, + "missed7": 4 + } + ], + "alerts": [ + { + "level": "info", + "title": "BMI in overweight range", + "message": "Your BMI of 25.9 is in the overweight range. Regular exercise and a balanced diet can help." + }, + { + "level": "warning", + "title": "Blood pressure often outside target range", + "message": "Only 11% of your readings are within the target range." + }, + { + "level": "success", + "title": "Excellent compliance: Ramipril", + "message": "100% compliance over the last 7 days. Keep it up!" + }, + { + "level": "warning", + "title": "Low compliance: Vitamin D", + "message": "Your 7-day compliance for Vitamin D is only 43%. Regular intake is important." + } + ], + "hasProvider": true, + "dataSpanDays": 90, + "totalMeasurements": 541, + "revalidating": false, + "ai": { + "briefing": { + "available": false, + "reason": "user_disabled", + "onDeviceAllowed": false + }, + "statusText": { + "available": false, + "reason": "user_disabled", + "onDeviceAllowed": false + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": null, + "meta": { + "cache.analytics.key_hash": 3572442229, + "cache.analytics.outcome": "hit" + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-comprehensive.06-operator-off.json.snap b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.06-operator-off.json.snap new file mode 100644 index 000000000..f9b98e98f --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.06-operator-off.json.snap @@ -0,0 +1,1274 @@ +{ + "status": 200, + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 58, + "latest": 82, + "min": 82, + "max": 84.4, + "mean": 83.19, + "median": null, + "avg7": 82.11, + "avg30": 82.59, + "slope7": { + "slope": -0.039, + "direction": "down", + "confidence": 0.89 + }, + "slope30": { + "slope": -0.04, + "direction": "down", + "confidence": 0.99 + }, + "anomalyCount": 0, + "avg30LastMonth": 83.8, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 87, + "latest": 123, + "min": 123, + "max": 137, + "mean": 130.2, + "median": null, + "avg7": 131.14, + "avg30": 130.1, + "slope7": { + "slope": -1.107, + "direction": "down", + "confidence": 0.22 + }, + "slope30": { + "slope": 0.043, + "direction": "up", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 130.24, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 87, + "latest": 78, + "min": 78, + "max": 86, + "mean": 81.94, + "median": null, + "avg7": 81.43, + "avg30": 81.66, + "slope7": { + "slope": -0.5, + "direction": "down", + "confidence": 0.17 + }, + "slope30": { + "slope": -0.019, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": 82.03, + "avg30LastYear": null + }, + "PULSE": { + "count": 58, + "latest": 64, + "min": 64, + "max": 69, + "mean": 66.57, + "median": null, + "avg7": 66.14, + "avg30": 66.55, + "slope7": { + "slope": -0.357, + "direction": "down", + "confidence": 0.16 + }, + "slope30": { + "slope": -0.035, + "direction": "down", + "confidence": 0.03 + }, + "anomalyCount": 0, + "avg30LastMonth": 66.59, + "avg30LastYear": null + }, + "BODY_FAT": { + "count": 8, + "latest": 24, + "min": 24, + "max": 25.1, + "mean": 24.61, + "median": null, + "avg7": 24, + "avg30": 24.33, + "slope7": null, + "slope30": { + "slope": -0.021, + "direction": "down", + "confidence": 0.99 + }, + "anomalyCount": 0, + "avg30LastMonth": 24.9, + "avg30LastYear": null + }, + "SLEEP_DURATION": { + "count": 81, + "latest": 12, + "min": 12, + "max": 240, + "mean": 98.64, + "median": null, + "avg7": 96.17, + "avg30": 98.64, + "slope7": { + "slope": -2.121, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.379, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVITY_STEPS": { + "count": 29, + "latest": 6000, + "min": 6000, + "max": 9833, + "mean": 7780.83, + "median": null, + "avg7": 7119, + "avg30": 7780.83, + "slope7": { + "slope": -373, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -34.049, + "direction": "down", + "confidence": 0.07 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BLOOD_GLUCOSE": { + "count": 57, + "latest": 128, + "min": 92, + "max": 147, + "mean": 113.12, + "median": null, + "avg7": 112.76, + "avg30": 113.12, + "slope7": { + "slope": -1.064, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.113, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "OXYGEN_SATURATION": { + "count": 9, + "latest": 96, + "min": 96, + "max": 98, + "mean": 96.89, + "median": null, + "avg7": 96.86, + "avg30": 96.89, + "slope7": { + "slope": -0.036, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.016, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HEART_RATE_VARIABILITY": { + "count": 29, + "latest": 45, + "min": 45, + "max": 52, + "mean": 48.17, + "median": null, + "avg7": 48, + "avg30": 48.17, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.046, + "direction": "down", + "confidence": 0.03 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESTING_HEART_RATE": { + "count": 38, + "latest": 58, + "min": 58, + "max": 61, + "mean": 59.45, + "median": null, + "avg7": 59.29, + "avg30": 59.38, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.08 + }, + "slope30": { + "slope": -0.009, + "direction": "stable", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 59.67, + "avg30LastYear": null + } + }, + "bmi": 25.9, + "bmiClassification": { + "category": "Overweight", + "color": "var(--warning)", + "severity": "warning" + }, + "bpClassification": { + "category": "High-normal", + "color": "var(--dracula-yellow)", + "severity": "elevated" + }, + "bpPctInTarget": 11, + "bpTargets": { + "sysLow": 120, + "sysHigh": 129, + "diaLow": 70, + "diaHigh": 79 + }, + "weightBpCorrelation": null, + "scatterData": [ + { + "weight": 84.4, + "sysBP": 131 + }, + { + "weight": 84.3, + "sysBP": 124 + }, + { + "weight": 84.3, + "sysBP": 132 + }, + { + "weight": 84.2, + "sysBP": 125 + }, + { + "weight": 84.2, + "sysBP": 133 + }, + { + "weight": 84.2, + "sysBP": 126 + }, + { + "weight": 84.1, + "sysBP": 134 + }, + { + "weight": 84.1, + "sysBP": 127 + }, + { + "weight": 84, + "sysBP": 135 + }, + { + "weight": 84, + "sysBP": 128 + }, + { + "weight": 84, + "sysBP": 136 + }, + { + "weight": 83.9, + "sysBP": 129 + }, + { + "weight": 83.9, + "sysBP": 137 + }, + { + "weight": 83.8, + "sysBP": 130 + }, + { + "weight": 83.8, + "sysBP": 123 + }, + { + "weight": 83.8, + "sysBP": 131 + }, + { + "weight": 83.7, + "sysBP": 124 + }, + { + "weight": 83.7, + "sysBP": 132 + }, + { + "weight": 83.6, + "sysBP": 125 + }, + { + "weight": 83.6, + "sysBP": 133 + }, + { + "weight": 83.6, + "sysBP": 126 + }, + { + "weight": 83.5, + "sysBP": 134 + }, + { + "weight": 83.5, + "sysBP": 127 + }, + { + "weight": 83.4, + "sysBP": 135 + }, + { + "weight": 83.4, + "sysBP": 128 + }, + { + "weight": 83.4, + "sysBP": 136 + }, + { + "weight": 83.3, + "sysBP": 129 + }, + { + "weight": 83.3, + "sysBP": 137 + }, + { + "weight": 83.2, + "sysBP": 130 + }, + { + "weight": 83.2, + "sysBP": 131 + }, + { + "weight": 83.1, + "sysBP": 124 + }, + { + "weight": 83.1, + "sysBP": 132 + }, + { + "weight": 83, + "sysBP": 125 + }, + { + "weight": 83, + "sysBP": 133 + }, + { + "weight": 83, + "sysBP": 126 + }, + { + "weight": 82.9, + "sysBP": 134 + }, + { + "weight": 82.9, + "sysBP": 127 + }, + { + "weight": 82.8, + "sysBP": 135 + }, + { + "weight": 82.8, + "sysBP": 128 + }, + { + "weight": 82.8, + "sysBP": 136 + }, + { + "weight": 82.7, + "sysBP": 129 + }, + { + "weight": 82.7, + "sysBP": 137 + }, + { + "weight": 82.6, + "sysBP": 130 + }, + { + "weight": 82.6, + "sysBP": 123 + }, + { + "weight": 82.6, + "sysBP": 131 + }, + { + "weight": 82.5, + "sysBP": 124 + }, + { + "weight": 82.5, + "sysBP": 132 + }, + { + "weight": 82.4, + "sysBP": 125 + }, + { + "weight": 82.4, + "sysBP": 133 + }, + { + "weight": 82.4, + "sysBP": 126 + }, + { + "weight": 82.3, + "sysBP": 134 + }, + { + "weight": 82.2, + "sysBP": 135 + }, + { + "weight": 82.2, + "sysBP": 128 + }, + { + "weight": 82.2, + "sysBP": 136 + }, + { + "weight": 82.1, + "sysBP": 129 + }, + { + "weight": 82.1, + "sysBP": 137 + }, + { + "weight": 82, + "sysBP": 130 + }, + { + "weight": 82, + "sysBP": 123 + } + ], + "bpMedicationCorrelation": null, + "bpMedicationScatterData": [ + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 124 + }, + { + "continuityPct": 0, + "sysBP": 132 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 126 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 127 + }, + { + "continuityPct": 0, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 129 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + }, + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 0, + "sysBP": 124 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 126 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 127 + }, + { + "continuityPct": 0, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + }, + { + "continuityPct": 100, + "sysBP": 123 + }, + { + "continuityPct": 0, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 124 + }, + { + "continuityPct": 100, + "sysBP": 132 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 129 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + } + ], + "moodSummary": { + "count": 45, + "latest": 1, + "min": 1, + "max": 5, + "mean": 3, + "median": 3, + "avg7": 2.86, + "avg30": 3, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": -0.013, + "direction": "down", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 3, + "avg30LastYear": null + }, + "moodBpCorrelation": null, + "moodBpScatterData": [ + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 2, + "sysBP": 127 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 2, + "sysBP": 127 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 1, + "sysBP": 123 + }, + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 1, + "sysBP": 123 + } + ], + "moodWeightCorrelation": null, + "moodWeightScatterData": [ + { + "mood": 3, + "weight": 83.8 + }, + { + "mood": 5, + "weight": 83.7 + }, + { + "mood": 2, + "weight": 83.7 + }, + { + "mood": 4, + "weight": 83.6 + }, + { + "mood": 1, + "weight": 83.6 + }, + { + "mood": 3, + "weight": 83.6 + }, + { + "mood": 5, + "weight": 83.5 + }, + { + "mood": 2, + "weight": 83.5 + }, + { + "mood": 4, + "weight": 83.4 + }, + { + "mood": 1, + "weight": 83.4 + }, + { + "mood": 3, + "weight": 83.4 + }, + { + "mood": 5, + "weight": 83.3 + }, + { + "mood": 2, + "weight": 83.3 + }, + { + "mood": 4, + "weight": 83.2 + }, + { + "mood": 3, + "weight": 83.2 + }, + { + "mood": 5, + "weight": 83.1 + }, + { + "mood": 2, + "weight": 83.1 + }, + { + "mood": 4, + "weight": 83 + }, + { + "mood": 1, + "weight": 83 + }, + { + "mood": 3, + "weight": 83 + }, + { + "mood": 5, + "weight": 82.9 + }, + { + "mood": 2, + "weight": 82.9 + }, + { + "mood": 4, + "weight": 82.8 + }, + { + "mood": 1, + "weight": 82.8 + }, + { + "mood": 3, + "weight": 82.8 + }, + { + "mood": 5, + "weight": 82.7 + }, + { + "mood": 2, + "weight": 82.7 + }, + { + "mood": 4, + "weight": 82.6 + }, + { + "mood": 1, + "weight": 82.6 + }, + { + "mood": 3, + "weight": 82.6 + }, + { + "mood": 5, + "weight": 82.5 + }, + { + "mood": 2, + "weight": 82.5 + }, + { + "mood": 4, + "weight": 82.4 + }, + { + "mood": 1, + "weight": 82.4 + }, + { + "mood": 3, + "weight": 82.4 + }, + { + "mood": 5, + "weight": 82.3 + }, + { + "mood": 4, + "weight": 82.2 + }, + { + "mood": 1, + "weight": 82.2 + }, + { + "mood": 3, + "weight": 82.2 + }, + { + "mood": 5, + "weight": 82.1 + }, + { + "mood": 2, + "weight": 82.1 + }, + { + "mood": 4, + "weight": 82 + }, + { + "mood": 1, + "weight": 82 + } + ], + "moodPulseCorrelation": null, + "moodPulseScatterData": [ + { + "mood": 3, + "pulse": 61 + }, + { + "mood": 5, + "pulse": 60 + }, + { + "mood": 2, + "pulse": 59 + }, + { + "mood": 4, + "pulse": 58 + }, + { + "mood": 1, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 60 + }, + { + "mood": 5, + "pulse": 59 + }, + { + "mood": 2, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 59 + }, + { + "mood": 5, + "pulse": 58 + }, + { + "mood": 2, + "pulse": 61 + }, + { + "mood": 4, + "pulse": 60 + }, + { + "mood": 1, + "pulse": 59 + }, + { + "mood": 3, + "pulse": 58 + }, + { + "mood": 5, + "pulse": 61 + }, + { + "mood": 2, + "pulse": 60 + }, + { + "mood": 4, + "pulse": 59 + }, + { + "mood": 1, + "pulse": 58 + }, + { + "mood": 3, + "pulse": 61 + }, + { + "mood": 5, + "pulse": 60 + }, + { + "mood": 2, + "pulse": 59 + }, + { + "mood": 4, + "pulse": 58 + }, + { + "mood": 1, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 60 + }, + { + "mood": 5, + "pulse": 59 + }, + { + "mood": 2, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 61 + }, + { + "mood": 1, + "pulse": 60 + }, + { + "mood": 3, + "pulse": 59 + }, + { + "mood": 5, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 60 + }, + { + "mood": 1, + "pulse": 59 + }, + { + "mood": 3, + "pulse": 58 + }, + { + "mood": 5, + "pulse": 61 + }, + { + "mood": 2, + "pulse": 60 + }, + { + "mood": 4, + "pulse": 59 + }, + { + "mood": 1, + "pulse": 58 + } + ], + "medications": [ + { + "id": "", + "name": "Ramipril", + "dose": "5mg", + "category": "BLOOD_PRESSURE", + "categoryLabel": null, + "compliance7": 100, + "compliance30": 76, + "streak": 0, + "taken7": 6, + "skipped7": 0, + "missed7": 0 + }, + { + "id": "", + "name": "Vitamin D", + "dose": "1000 IU", + "category": "OTHER", + "categoryLabel": null, + "compliance7": 43, + "compliance30": 47, + "streak": 1, + "taken7": 3, + "skipped7": 0, + "missed7": 4 + } + ], + "alerts": [ + { + "level": "info", + "title": "BMI in overweight range", + "message": "Your BMI of 25.9 is in the overweight range. Regular exercise and a balanced diet can help." + }, + { + "level": "warning", + "title": "Blood pressure often outside target range", + "message": "Only 11% of your readings are within the target range." + }, + { + "level": "success", + "title": "Excellent compliance: Ramipril", + "message": "100% compliance over the last 7 days. Keep it up!" + }, + { + "level": "warning", + "title": "Low compliance: Vitamin D", + "message": "Your 7-day compliance for Vitamin D is only 43%. Regular intake is important." + } + ], + "hasProvider": true, + "dataSpanDays": 90, + "totalMeasurements": 541, + "revalidating": false, + "ai": { + "briefing": { + "available": false, + "reason": "operator_disabled", + "onDeviceAllowed": false + }, + "statusText": { + "available": false, + "reason": "operator_disabled", + "onDeviceAllowed": false + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": null, + "meta": { + "cache.analytics.key_hash": 3572442229, + "cache.analytics.outcome": "hit" + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-comprehensive.07-manage-delegate.json.snap b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.07-manage-delegate.json.snap new file mode 100644 index 000000000..06218e2c0 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.07-manage-delegate.json.snap @@ -0,0 +1,1303 @@ +{ + "status": 200, + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 58, + "latest": 82, + "min": 82, + "max": 84.4, + "mean": 83.19, + "median": null, + "avg7": 82.11, + "avg30": 82.59, + "slope7": { + "slope": -0.039, + "direction": "down", + "confidence": 0.89 + }, + "slope30": { + "slope": -0.04, + "direction": "down", + "confidence": 0.99 + }, + "anomalyCount": 0, + "avg30LastMonth": 83.8, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 87, + "latest": 123, + "min": 123, + "max": 137, + "mean": 130.2, + "median": null, + "avg7": 131.14, + "avg30": 130.1, + "slope7": { + "slope": -1.107, + "direction": "down", + "confidence": 0.22 + }, + "slope30": { + "slope": 0.043, + "direction": "up", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 130.24, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 87, + "latest": 78, + "min": 78, + "max": 86, + "mean": 81.94, + "median": null, + "avg7": 81.43, + "avg30": 81.66, + "slope7": { + "slope": -0.5, + "direction": "down", + "confidence": 0.17 + }, + "slope30": { + "slope": -0.019, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": 82.03, + "avg30LastYear": null + }, + "PULSE": { + "count": 58, + "latest": 64, + "min": 64, + "max": 69, + "mean": 66.57, + "median": null, + "avg7": 66.14, + "avg30": 66.55, + "slope7": { + "slope": -0.357, + "direction": "down", + "confidence": 0.16 + }, + "slope30": { + "slope": -0.035, + "direction": "down", + "confidence": 0.03 + }, + "anomalyCount": 0, + "avg30LastMonth": 66.59, + "avg30LastYear": null + }, + "BODY_FAT": { + "count": 8, + "latest": 24, + "min": 24, + "max": 25.1, + "mean": 24.61, + "median": null, + "avg7": 24, + "avg30": 24.33, + "slope7": null, + "slope30": { + "slope": -0.021, + "direction": "down", + "confidence": 0.99 + }, + "anomalyCount": 0, + "avg30LastMonth": 24.9, + "avg30LastYear": null + }, + "SLEEP_DURATION": { + "count": 81, + "latest": 12, + "min": 12, + "max": 240, + "mean": 98.64, + "median": null, + "avg7": 96.17, + "avg30": 98.64, + "slope7": { + "slope": -2.121, + "direction": "down", + "confidence": 0 + }, + "slope30": { + "slope": -0.379, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "ACTIVITY_STEPS": { + "count": 29, + "latest": 6000, + "min": 6000, + "max": 9833, + "mean": 7780.83, + "median": null, + "avg7": 7119, + "avg30": 7780.83, + "slope7": { + "slope": -373, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -34.049, + "direction": "down", + "confidence": 0.07 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "BLOOD_GLUCOSE": { + "count": 57, + "latest": 128, + "min": 92, + "max": 147, + "mean": 113.12, + "median": null, + "avg7": 112.76, + "avg30": 113.12, + "slope7": { + "slope": -1.064, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.113, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "OXYGEN_SATURATION": { + "count": 9, + "latest": 96, + "min": 96, + "max": 98, + "mean": 96.89, + "median": null, + "avg7": 96.86, + "avg30": 96.89, + "slope7": { + "slope": -0.036, + "direction": "down", + "confidence": 0.01 + }, + "slope30": { + "slope": -0.016, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "HEART_RATE_VARIABILITY": { + "count": 29, + "latest": 45, + "min": 45, + "max": 52, + "mean": 48.17, + "median": null, + "avg7": 48, + "avg30": 48.17, + "slope7": { + "slope": -1, + "direction": "down", + "confidence": 1 + }, + "slope30": { + "slope": -0.046, + "direction": "down", + "confidence": 0.03 + }, + "anomalyCount": 0, + "avg30LastMonth": null, + "avg30LastYear": null + }, + "RESTING_HEART_RATE": { + "count": 38, + "latest": 58, + "min": 58, + "max": 61, + "mean": 59.45, + "median": null, + "avg7": 59.29, + "avg30": 59.38, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.08 + }, + "slope30": { + "slope": -0.009, + "direction": "stable", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 59.67, + "avg30LastYear": null + } + }, + "bmi": 25.9, + "bmiClassification": { + "category": "Overweight", + "color": "var(--warning)", + "severity": "warning" + }, + "bpClassification": { + "category": "High-normal", + "color": "var(--dracula-yellow)", + "severity": "elevated" + }, + "bpPctInTarget": 11, + "bpTargets": { + "sysLow": 120, + "sysHigh": 129, + "diaLow": 70, + "diaHigh": 79 + }, + "weightBpCorrelation": null, + "scatterData": [ + { + "weight": 84.4, + "sysBP": 131 + }, + { + "weight": 84.3, + "sysBP": 124 + }, + { + "weight": 84.3, + "sysBP": 132 + }, + { + "weight": 84.2, + "sysBP": 125 + }, + { + "weight": 84.2, + "sysBP": 133 + }, + { + "weight": 84.2, + "sysBP": 126 + }, + { + "weight": 84.1, + "sysBP": 134 + }, + { + "weight": 84.1, + "sysBP": 127 + }, + { + "weight": 84, + "sysBP": 135 + }, + { + "weight": 84, + "sysBP": 128 + }, + { + "weight": 84, + "sysBP": 136 + }, + { + "weight": 83.9, + "sysBP": 129 + }, + { + "weight": 83.9, + "sysBP": 137 + }, + { + "weight": 83.8, + "sysBP": 130 + }, + { + "weight": 83.8, + "sysBP": 123 + }, + { + "weight": 83.8, + "sysBP": 131 + }, + { + "weight": 83.7, + "sysBP": 124 + }, + { + "weight": 83.7, + "sysBP": 132 + }, + { + "weight": 83.6, + "sysBP": 125 + }, + { + "weight": 83.6, + "sysBP": 133 + }, + { + "weight": 83.6, + "sysBP": 126 + }, + { + "weight": 83.5, + "sysBP": 134 + }, + { + "weight": 83.5, + "sysBP": 127 + }, + { + "weight": 83.4, + "sysBP": 135 + }, + { + "weight": 83.4, + "sysBP": 128 + }, + { + "weight": 83.4, + "sysBP": 136 + }, + { + "weight": 83.3, + "sysBP": 129 + }, + { + "weight": 83.3, + "sysBP": 137 + }, + { + "weight": 83.2, + "sysBP": 130 + }, + { + "weight": 83.2, + "sysBP": 131 + }, + { + "weight": 83.1, + "sysBP": 124 + }, + { + "weight": 83.1, + "sysBP": 132 + }, + { + "weight": 83, + "sysBP": 125 + }, + { + "weight": 83, + "sysBP": 133 + }, + { + "weight": 83, + "sysBP": 126 + }, + { + "weight": 82.9, + "sysBP": 134 + }, + { + "weight": 82.9, + "sysBP": 127 + }, + { + "weight": 82.8, + "sysBP": 135 + }, + { + "weight": 82.8, + "sysBP": 128 + }, + { + "weight": 82.8, + "sysBP": 136 + }, + { + "weight": 82.7, + "sysBP": 129 + }, + { + "weight": 82.7, + "sysBP": 137 + }, + { + "weight": 82.6, + "sysBP": 130 + }, + { + "weight": 82.6, + "sysBP": 123 + }, + { + "weight": 82.6, + "sysBP": 131 + }, + { + "weight": 82.5, + "sysBP": 124 + }, + { + "weight": 82.5, + "sysBP": 132 + }, + { + "weight": 82.4, + "sysBP": 125 + }, + { + "weight": 82.4, + "sysBP": 133 + }, + { + "weight": 82.4, + "sysBP": 126 + }, + { + "weight": 82.3, + "sysBP": 134 + }, + { + "weight": 82.2, + "sysBP": 135 + }, + { + "weight": 82.2, + "sysBP": 128 + }, + { + "weight": 82.2, + "sysBP": 136 + }, + { + "weight": 82.1, + "sysBP": 129 + }, + { + "weight": 82.1, + "sysBP": 137 + }, + { + "weight": 82, + "sysBP": 130 + }, + { + "weight": 82, + "sysBP": 123 + } + ], + "bpMedicationCorrelation": null, + "bpMedicationScatterData": [ + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 124 + }, + { + "continuityPct": 0, + "sysBP": 132 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 126 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 127 + }, + { + "continuityPct": 0, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 129 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + }, + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 0, + "sysBP": 124 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 126 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 127 + }, + { + "continuityPct": 0, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + }, + { + "continuityPct": 100, + "sysBP": 123 + }, + { + "continuityPct": 0, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 124 + }, + { + "continuityPct": 100, + "sysBP": 132 + }, + { + "continuityPct": 100, + "sysBP": 125 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 128 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 129 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 130 + } + ], + "moodSummary": { + "count": 45, + "latest": 1, + "min": 1, + "max": 5, + "mean": 3, + "median": 3, + "avg7": 2.86, + "avg30": 3, + "slope7": { + "slope": -0.143, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": -0.013, + "direction": "down", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 3, + "avg30LastYear": null + }, + "moodBpCorrelation": null, + "moodBpScatterData": [ + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 2, + "sysBP": 127 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 2, + "sysBP": 127 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 1, + "sysBP": 123 + }, + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 5, + "sysBP": 124 + }, + { + "mood": 2, + "sysBP": 132 + }, + { + "mood": 4, + "sysBP": 125 + }, + { + "mood": 1, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 126 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 4, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 128 + }, + { + "mood": 3, + "sysBP": 136 + }, + { + "mood": 5, + "sysBP": 129 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 4, + "sysBP": 130 + }, + { + "mood": 1, + "sysBP": 123 + } + ], + "moodWeightCorrelation": null, + "moodWeightScatterData": [ + { + "mood": 3, + "weight": 83.8 + }, + { + "mood": 5, + "weight": 83.7 + }, + { + "mood": 2, + "weight": 83.7 + }, + { + "mood": 4, + "weight": 83.6 + }, + { + "mood": 1, + "weight": 83.6 + }, + { + "mood": 3, + "weight": 83.6 + }, + { + "mood": 5, + "weight": 83.5 + }, + { + "mood": 2, + "weight": 83.5 + }, + { + "mood": 4, + "weight": 83.4 + }, + { + "mood": 1, + "weight": 83.4 + }, + { + "mood": 3, + "weight": 83.4 + }, + { + "mood": 5, + "weight": 83.3 + }, + { + "mood": 2, + "weight": 83.3 + }, + { + "mood": 4, + "weight": 83.2 + }, + { + "mood": 3, + "weight": 83.2 + }, + { + "mood": 5, + "weight": 83.1 + }, + { + "mood": 2, + "weight": 83.1 + }, + { + "mood": 4, + "weight": 83 + }, + { + "mood": 1, + "weight": 83 + }, + { + "mood": 3, + "weight": 83 + }, + { + "mood": 5, + "weight": 82.9 + }, + { + "mood": 2, + "weight": 82.9 + }, + { + "mood": 4, + "weight": 82.8 + }, + { + "mood": 1, + "weight": 82.8 + }, + { + "mood": 3, + "weight": 82.8 + }, + { + "mood": 5, + "weight": 82.7 + }, + { + "mood": 2, + "weight": 82.7 + }, + { + "mood": 4, + "weight": 82.6 + }, + { + "mood": 1, + "weight": 82.6 + }, + { + "mood": 3, + "weight": 82.6 + }, + { + "mood": 5, + "weight": 82.5 + }, + { + "mood": 2, + "weight": 82.5 + }, + { + "mood": 4, + "weight": 82.4 + }, + { + "mood": 1, + "weight": 82.4 + }, + { + "mood": 3, + "weight": 82.4 + }, + { + "mood": 5, + "weight": 82.3 + }, + { + "mood": 4, + "weight": 82.2 + }, + { + "mood": 1, + "weight": 82.2 + }, + { + "mood": 3, + "weight": 82.2 + }, + { + "mood": 5, + "weight": 82.1 + }, + { + "mood": 2, + "weight": 82.1 + }, + { + "mood": 4, + "weight": 82 + }, + { + "mood": 1, + "weight": 82 + } + ], + "moodPulseCorrelation": null, + "moodPulseScatterData": [ + { + "mood": 3, + "pulse": 61 + }, + { + "mood": 5, + "pulse": 60 + }, + { + "mood": 2, + "pulse": 59 + }, + { + "mood": 4, + "pulse": 58 + }, + { + "mood": 1, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 60 + }, + { + "mood": 5, + "pulse": 59 + }, + { + "mood": 2, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 59 + }, + { + "mood": 5, + "pulse": 58 + }, + { + "mood": 2, + "pulse": 61 + }, + { + "mood": 4, + "pulse": 60 + }, + { + "mood": 1, + "pulse": 59 + }, + { + "mood": 3, + "pulse": 58 + }, + { + "mood": 5, + "pulse": 61 + }, + { + "mood": 2, + "pulse": 60 + }, + { + "mood": 4, + "pulse": 59 + }, + { + "mood": 1, + "pulse": 58 + }, + { + "mood": 3, + "pulse": 61 + }, + { + "mood": 5, + "pulse": 60 + }, + { + "mood": 2, + "pulse": 59 + }, + { + "mood": 4, + "pulse": 58 + }, + { + "mood": 1, + "pulse": 61 + }, + { + "mood": 3, + "pulse": 60 + }, + { + "mood": 5, + "pulse": 59 + }, + { + "mood": 2, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 61 + }, + { + "mood": 1, + "pulse": 60 + }, + { + "mood": 3, + "pulse": 59 + }, + { + "mood": 5, + "pulse": 58 + }, + { + "mood": 4, + "pulse": 60 + }, + { + "mood": 1, + "pulse": 59 + }, + { + "mood": 3, + "pulse": 58 + }, + { + "mood": 5, + "pulse": 61 + }, + { + "mood": 2, + "pulse": 60 + }, + { + "mood": 4, + "pulse": 59 + }, + { + "mood": 1, + "pulse": 58 + } + ], + "medications": [ + { + "id": "", + "name": "Ramipril", + "dose": "5mg", + "category": "BLOOD_PRESSURE", + "categoryLabel": null, + "compliance7": 100, + "compliance30": 76, + "streak": 0, + "taken7": 6, + "skipped7": 0, + "missed7": 0 + }, + { + "id": "", + "name": "Vitamin D", + "dose": "1000 IU", + "category": "OTHER", + "categoryLabel": null, + "compliance7": 43, + "compliance30": 47, + "streak": 1, + "taken7": 3, + "skipped7": 0, + "missed7": 4 + } + ], + "alerts": [ + { + "level": "info", + "title": "BMI in overweight range", + "message": "Your BMI of 25.9 is in the overweight range. Regular exercise and a balanced diet can help." + }, + { + "level": "warning", + "title": "Blood pressure often outside target range", + "message": "Only 11% of your readings are within the target range." + }, + { + "level": "success", + "title": "Excellent compliance: Ramipril", + "message": "100% compliance over the last 7 days. Keep it up!" + }, + { + "level": "warning", + "title": "Low compliance: Vitamin D", + "message": "Your 7-day compliance for Vitamin D is only 43%. Regular intake is important." + } + ], + "hasProvider": false, + "dataSpanDays": 90, + "totalMeasurements": 541, + "revalidating": false, + "ai": { + "briefing": { + "available": true, + "reason": null, + "onDeviceAllowed": true + }, + "statusText": { + "available": true, + "reason": null, + "onDeviceAllowed": true + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.comprehensive" + }, + "meta": { + "cache.analytics.key_hash": 3572442229, + "cache.analytics.outcome": "miss", + "insights": { + "slope_source": "rollup", + "anomaly_source": "live", + "regression_source": "rollup" + }, + "insights.sub_bpRaw_ms": "", + "insights.sub_buckets_ms": "", + "insights.sub_coverage_ms": "", + "insights.sub_firstAt_ms": "", + "insights.sub_latests_ms": "", + "insights.sub_narrow_ms": "", + "medications": 2, + "moodEntries": 45, + "totalMeasurements": 541 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-comprehensive.08-read-delegate.json.snap b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.08-read-delegate.json.snap new file mode 100644 index 000000000..b4e2a65a9 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.08-read-delegate.json.snap @@ -0,0 +1,26 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "Account access denied", + "meta": { + "errorCode": "sharing.access.denied", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": null, + "meta": { + "sharing_refusal": "insufficient_access" + }, + "warnings": null, + "error": { + "type": "SharingAccessDeniedError", + "message": "Account access denied" + } + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-comprehensive.09-rate-limited.json.snap b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.09-rate-limited.json.snap new file mode 100644 index 000000000..6f6b705b2 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.09-rate-limited.json.snap @@ -0,0 +1,21 @@ +{ + "status": 429, + "body": { + "data": null, + "error": "Too many analytics requests. Please retry later.", + "meta": { + "errorCode": "http.rate_limited", + "requestId": "" + } + }, + "events": [ + { + "status": 429, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-comprehensive.10-sparse.json.snap b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.10-sparse.json.snap new file mode 100644 index 000000000..d2362f2ef --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.10-sparse.json.snap @@ -0,0 +1,884 @@ +{ + "status": 200, + "body": { + "data": { + "summaries": { + "WEIGHT": { + "count": 47, + "latest": 70.3, + "min": 70, + "max": 71.2, + "mean": 70.63, + "median": null, + "avg7": 70.55, + "avg30": 70.62, + "slope7": { + "slope": 0.043, + "direction": "up", + "confidence": 0.03 + }, + "slope30": { + "slope": -0.004, + "direction": "stable", + "confidence": 0.01 + }, + "anomalyCount": 0, + "avg30LastMonth": 70.63, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_SYS": { + "count": 47, + "latest": 134, + "min": 131, + "max": 163, + "mean": 139.66, + "median": null, + "avg7": 139.67, + "avg30": 139.93, + "slope7": { + "slope": -2.057, + "direction": "down", + "confidence": 0.18 + }, + "slope30": { + "slope": -0.03, + "direction": "down", + "confidence": 0 + }, + "anomalyCount": 4, + "avg30LastMonth": 139.26, + "avg30LastYear": null + }, + "BLOOD_PRESSURE_DIA": { + "count": 47, + "latest": 86, + "min": 84, + "max": 90, + "mean": 87.04, + "median": null, + "avg7": 87.5, + "avg30": 87.07, + "slope7": { + "slope": -0.2, + "direction": "down", + "confidence": 0.04 + }, + "slope30": { + "slope": 0.034, + "direction": "up", + "confidence": 0.02 + }, + "anomalyCount": 0, + "avg30LastMonth": 87, + "avg30LastYear": null + }, + "PULSE": { + "count": 141, + "latest": 93, + "min": 66, + "max": 96, + "mean": 81.67, + "median": null, + "avg7": 81.61, + "avg30": 81.61, + "slope7": { + "slope": 0.186, + "direction": "up", + "confidence": 0 + }, + "slope30": { + "slope": 0.027, + "direction": "up", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": 81.75, + "avg30LastYear": null + } + }, + "bmi": null, + "bmiClassification": null, + "bpClassification": { + "category": "High-normal", + "color": "var(--dracula-yellow)", + "severity": "elevated" + }, + "bpPctInTarget": 0, + "bpTargets": { + "sysLow": 130, + "sysHigh": 139, + "diaLow": 70, + "diaHigh": 79 + }, + "weightBpCorrelation": { + "r": -0.631, + "strength": "moderat", + "n": 47 + }, + "scatterData": [ + { + "weight": 71.2, + "sysBP": 135 + }, + { + "weight": 70.9, + "sysBP": 132 + }, + { + "weight": 70.6, + "sysBP": 140 + }, + { + "weight": 70.3, + "sysBP": 137 + }, + { + "weight": 70, + "sysBP": 156 + }, + { + "weight": 71.2, + "sysBP": 131 + }, + { + "weight": 70.9, + "sysBP": 139 + }, + { + "weight": 70.6, + "sysBP": 136 + }, + { + "weight": 70.3, + "sysBP": 133 + }, + { + "weight": 70, + "sysBP": 163 + }, + { + "weight": 71.2, + "sysBP": 138 + }, + { + "weight": 70.9, + "sysBP": 135 + }, + { + "weight": 70.6, + "sysBP": 132 + }, + { + "weight": 70.3, + "sysBP": 140 + }, + { + "weight": 70, + "sysBP": 159 + }, + { + "weight": 71.2, + "sysBP": 134 + }, + { + "weight": 70.9, + "sysBP": 131 + }, + { + "weight": 70.6, + "sysBP": 139 + }, + { + "weight": 70.3, + "sysBP": 136 + }, + { + "weight": 71.2, + "sysBP": 141 + }, + { + "weight": 70.9, + "sysBP": 138 + }, + { + "weight": 70.6, + "sysBP": 135 + }, + { + "weight": 70.3, + "sysBP": 132 + }, + { + "weight": 70, + "sysBP": 162 + }, + { + "weight": 71.2, + "sysBP": 137 + }, + { + "weight": 70.9, + "sysBP": 134 + }, + { + "weight": 70.6, + "sysBP": 131 + }, + { + "weight": 70.3, + "sysBP": 139 + }, + { + "weight": 70, + "sysBP": 158 + }, + { + "weight": 71.2, + "sysBP": 133 + }, + { + "weight": 70.9, + "sysBP": 141 + }, + { + "weight": 70.6, + "sysBP": 138 + }, + { + "weight": 70.3, + "sysBP": 135 + }, + { + "weight": 70, + "sysBP": 154 + }, + { + "weight": 71.2, + "sysBP": 140 + }, + { + "weight": 70.9, + "sysBP": 137 + }, + { + "weight": 70.6, + "sysBP": 134 + }, + { + "weight": 70.3, + "sysBP": 131 + }, + { + "weight": 70, + "sysBP": 161 + }, + { + "weight": 71.2, + "sysBP": 136 + }, + { + "weight": 70.9, + "sysBP": 133 + }, + { + "weight": 70.3, + "sysBP": 138 + }, + { + "weight": 70, + "sysBP": 157 + }, + { + "weight": 71.2, + "sysBP": 132 + }, + { + "weight": 70.9, + "sysBP": 140 + }, + { + "weight": 70.6, + "sysBP": 137 + }, + { + "weight": 70.3, + "sysBP": 134 + } + ], + "bpMedicationCorrelation": { + "r": -0.941, + "strength": "stark", + "n": 28, + "medicationCount": 1 + }, + "bpMedicationScatterData": [ + { + "continuityPct": 100, + "sysBP": 138 + }, + { + "continuityPct": 100, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 132 + }, + { + "continuityPct": 0, + "sysBP": 159 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 141 + }, + { + "continuityPct": 100, + "sysBP": 135 + }, + { + "continuityPct": 100, + "sysBP": 132 + }, + { + "continuityPct": 0, + "sysBP": 162 + }, + { + "continuityPct": 100, + "sysBP": 134 + }, + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 100, + "sysBP": 139 + }, + { + "continuityPct": 100, + "sysBP": 133 + }, + { + "continuityPct": 100, + "sysBP": 141 + }, + { + "continuityPct": 100, + "sysBP": 138 + }, + { + "continuityPct": 0, + "sysBP": 154 + }, + { + "continuityPct": 100, + "sysBP": 140 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 131 + }, + { + "continuityPct": 0, + "sysBP": 161 + }, + { + "continuityPct": 100, + "sysBP": 136 + }, + { + "continuityPct": 100, + "sysBP": 138 + }, + { + "continuityPct": 0, + "sysBP": 157 + }, + { + "continuityPct": 100, + "sysBP": 140 + }, + { + "continuityPct": 100, + "sysBP": 137 + }, + { + "continuityPct": 100, + "sysBP": 134 + } + ], + "moodSummary": { + "count": 29, + "latest": 3, + "min": 1, + "max": 5, + "mean": 3.07, + "median": 3, + "avg7": 3, + "avg30": 3.04, + "slope7": { + "slope": 0.286, + "direction": "up", + "confidence": 0.14 + }, + "slope30": { + "slope": 0, + "direction": "stable", + "confidence": 0 + }, + "anomalyCount": 0, + "avg30LastMonth": 4, + "avg30LastYear": null + }, + "moodBpCorrelation": { + "r": -0.683, + "strength": "moderat", + "n": 28 + }, + "moodBpScatterData": [ + { + "mood": 4, + "sysBP": 141 + }, + { + "mood": 2, + "sysBP": 138 + }, + { + "mood": 5, + "sysBP": 135 + }, + { + "mood": 3, + "sysBP": 132 + }, + { + "mood": 1, + "sysBP": 162 + }, + { + "mood": 4, + "sysBP": 137 + }, + { + "mood": 2, + "sysBP": 134 + }, + { + "mood": 5, + "sysBP": 131 + }, + { + "mood": 3, + "sysBP": 139 + }, + { + "mood": 1, + "sysBP": 158 + }, + { + "mood": 4, + "sysBP": 133 + }, + { + "mood": 2, + "sysBP": 141 + }, + { + "mood": 5, + "sysBP": 138 + }, + { + "mood": 3, + "sysBP": 135 + }, + { + "mood": 1, + "sysBP": 154 + }, + { + "mood": 4, + "sysBP": 140 + }, + { + "mood": 2, + "sysBP": 137 + }, + { + "mood": 5, + "sysBP": 134 + }, + { + "mood": 3, + "sysBP": 131 + }, + { + "mood": 1, + "sysBP": 161 + }, + { + "mood": 4, + "sysBP": 136 + }, + { + "mood": 2, + "sysBP": 133 + }, + { + "mood": 3, + "sysBP": 138 + }, + { + "mood": 1, + "sysBP": 157 + }, + { + "mood": 4, + "sysBP": 132 + }, + { + "mood": 2, + "sysBP": 140 + }, + { + "mood": 5, + "sysBP": 137 + }, + { + "mood": 3, + "sysBP": 134 + } + ], + "moodWeightCorrelation": { + "r": 0.482, + "strength": "moderat", + "n": 28 + }, + "moodWeightScatterData": [ + { + "mood": 4, + "weight": 71.2 + }, + { + "mood": 2, + "weight": 70.9 + }, + { + "mood": 5, + "weight": 70.6 + }, + { + "mood": 3, + "weight": 70.3 + }, + { + "mood": 1, + "weight": 70 + }, + { + "mood": 4, + "weight": 71.2 + }, + { + "mood": 2, + "weight": 70.9 + }, + { + "mood": 5, + "weight": 70.6 + }, + { + "mood": 3, + "weight": 70.3 + }, + { + "mood": 1, + "weight": 70 + }, + { + "mood": 4, + "weight": 71.2 + }, + { + "mood": 2, + "weight": 70.9 + }, + { + "mood": 5, + "weight": 70.6 + }, + { + "mood": 3, + "weight": 70.3 + }, + { + "mood": 1, + "weight": 70 + }, + { + "mood": 4, + "weight": 71.2 + }, + { + "mood": 2, + "weight": 70.9 + }, + { + "mood": 5, + "weight": 70.6 + }, + { + "mood": 3, + "weight": 70.3 + }, + { + "mood": 1, + "weight": 70 + }, + { + "mood": 4, + "weight": 71.2 + }, + { + "mood": 2, + "weight": 70.9 + }, + { + "mood": 3, + "weight": 70.3 + }, + { + "mood": 1, + "weight": 70 + }, + { + "mood": 4, + "weight": 71.2 + }, + { + "mood": 2, + "weight": 70.9 + }, + { + "mood": 5, + "weight": 70.6 + }, + { + "mood": 3, + "weight": 70.3 + } + ], + "moodPulseCorrelation": null, + "moodPulseScatterData": [ + { + "mood": 2, + "pulse": 72 + }, + { + "mood": 5, + "pulse": 71 + }, + { + "mood": 3, + "pulse": 78 + }, + { + "mood": 1, + "pulse": 77 + }, + { + "mood": 4, + "pulse": 76 + }, + { + "mood": 2, + "pulse": 75 + }, + { + "mood": 5, + "pulse": 74 + }, + { + "mood": 3, + "pulse": 73 + }, + { + "mood": 1, + "pulse": 72 + }, + { + "mood": 4, + "pulse": 74 + }, + { + "mood": 2, + "pulse": 73 + }, + { + "mood": 5, + "pulse": 77 + }, + { + "mood": 3, + "pulse": 76 + }, + { + "mood": 1, + "pulse": 75 + }, + { + "mood": 4, + "pulse": 74 + }, + { + "mood": 2, + "pulse": 73 + }, + { + "mood": 5, + "pulse": 75 + }, + { + "mood": 3, + "pulse": 74 + }, + { + "mood": 1, + "pulse": 73 + }, + { + "mood": 4, + "pulse": 72 + }, + { + "mood": 2, + "pulse": 76 + }, + { + "mood": 1, + "pulse": 76 + }, + { + "mood": 4, + "pulse": 75 + }, + { + "mood": 2, + "pulse": 74 + }, + { + "mood": 5, + "pulse": 73 + }, + { + "mood": 3, + "pulse": 72 + } + ], + "medications": [ + { + "id": "", + "name": "Amlodipine", + "dose": "5mg", + "category": "BLOOD_PRESSURE", + "categoryLabel": null, + "compliance7": 67, + "compliance30": 65, + "streak": 4, + "taken7": 4, + "skipped7": 1, + "missed7": 2 + } + ], + "alerts": [ + { + "level": "warning", + "title": "Blood pressure often outside target range", + "message": "Only 0% of your readings are within the target range." + }, + { + "level": "warning", + "title": "Low compliance: Amlodipine", + "message": "Your 7-day compliance for Amlodipine is only 67%. Regular intake is important." + } + ], + "hasProvider": false, + "dataSpanDays": 50, + "totalMeasurements": 282, + "revalidating": false, + "ai": { + "briefing": { + "available": false, + "reason": "no_provider", + "onDeviceAllowed": true + }, + "statusText": { + "available": false, + "reason": "no_provider", + "onDeviceAllowed": true + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.comprehensive" + }, + "meta": { + "cache.analytics.key_hash": 2016842294, + "cache.analytics.outcome": "miss", + "insights": { + "slope_source": "rollup", + "anomaly_source": "live", + "regression_source": "rollup" + }, + "insights.sub_bpRaw_ms": "", + "insights.sub_buckets_ms": "", + "insights.sub_coverage_ms": "", + "insights.sub_firstAt_ms": "", + "insights.sub_latests_ms": "", + "insights.sub_narrow_ms": "", + "medications": 1, + "moodEntries": 29, + "totalMeasurements": 282 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-comprehensive.11-empty.json.snap b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.11-empty.json.snap new file mode 100644 index 000000000..ecaa73274 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-comprehensive.11-empty.json.snap @@ -0,0 +1,79 @@ +{ + "status": 200, + "body": { + "data": { + "summaries": {}, + "bmi": null, + "bmiClassification": null, + "bpClassification": null, + "bpPctInTarget": null, + "bpTargets": null, + "weightBpCorrelation": null, + "scatterData": [], + "bpMedicationCorrelation": null, + "bpMedicationScatterData": [], + "moodSummary": null, + "moodBpCorrelation": null, + "moodBpScatterData": [], + "moodWeightCorrelation": null, + "moodWeightScatterData": [], + "moodPulseCorrelation": null, + "moodPulseScatterData": [], + "medications": [], + "alerts": [], + "hasProvider": false, + "dataSpanDays": 0, + "totalMeasurements": 0, + "revalidating": false, + "ai": { + "briefing": { + "available": false, + "reason": "no_provider", + "onDeviceAllowed": true + }, + "statusText": { + "available": false, + "reason": "no_provider", + "onDeviceAllowed": true + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.comprehensive" + }, + "meta": { + "cache.analytics.key_hash": 461242359, + "cache.analytics.outcome": "miss", + "insights.sub_bpRaw_ms": "", + "insights.sub_buckets_ms": "", + "insights.sub_coverage_ms": "", + "insights.sub_heavy_ms": "", + "insights.sub_latests_ms": "", + "medications": 0, + "moodEntries": 0, + "totalMeasurements": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G01-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G01-anonymous.json.snap new file mode 100644 index 000000000..694a6f447 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G01-anonymous.json.snap @@ -0,0 +1,32 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G02-opted-out.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G02-opted-out.json.snap new file mode 100644 index 000000000..b234a3fe3 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G02-opted-out.json.snap @@ -0,0 +1,35 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "This AI feature is turned off in your settings", + "meta": { + "capability": "briefing", + "reason": "user_disabled", + "errorCode": "module.disabled", + "module": "insights", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "AiUnavailableError", + "message": "This AI feature is turned off in your settings" + } + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G03-empty.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G03-empty.json.snap new file mode 100644 index 000000000..0ef9af169 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G03-empty.json.snap @@ -0,0 +1,52 @@ +{ + "status": 200, + "body": { + "data": { + "insights": null, + "cached": false, + "legacyPayload": false, + "revalidating": true, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": false, + "hasProvider": true, + "languageMismatch": false, + "readerLocale": "en", + "revalidating": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueForceWarm", + "args": [ + { + "userId": "", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G04-fresh.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G04-fresh.json.snap new file mode 100644 index 000000000..0798c38b0 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G04-fresh.json.snap @@ -0,0 +1,92 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "cached": true, + "cachedAt": "anchor+0d+11:00:00", + "legacyPayload": false, + "revalidating": false, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": true, + "hasProvider": true, + "legacyPayload": false, + "revalidating": false + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": "anchor+0d+11:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G05-stale.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G05-stale.json.snap new file mode 100644 index 000000000..fe446ac21 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G05-stale.json.snap @@ -0,0 +1,90 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor-1d+16:00:00" + }, + "cached": true, + "cachedAt": "anchor+0d+10:00:00", + "legacyPayload": false, + "revalidating": true, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": true, + "hasProvider": true, + "legacyPayload": false, + "revalidating": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueForceWarm", + "args": [ + { + "userId": "", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+10:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor-1d+16:00:00" + }, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G06-other-language.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G06-other-language.json.snap new file mode 100644 index 000000000..5e2b774d1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G06-other-language.json.snap @@ -0,0 +1,77 @@ +{ + "status": 200, + "body": { + "data": { + "insights": null, + "cached": false, + "legacyPayload": false, + "revalidating": true, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": false, + "hasProvider": true, + "languageMismatch": true, + "readerLocale": "en", + "revalidating": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueForceWarm", + "args": [ + { + "userId": "", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+11:00:00", + "insightsCachedLocale": "de", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G07-unparseable.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G07-unparseable.json.snap new file mode 100644 index 000000000..c09a6729d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G07-unparseable.json.snap @@ -0,0 +1,52 @@ +{ + "status": 200, + "body": { + "data": { + "insights": null, + "cached": false, + "legacyPayload": false, + "revalidating": true, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": false, + "hasProvider": true, + "languageMismatch": false, + "readerLocale": "en", + "revalidating": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueForceWarm", + "args": [ + { + "userId": "", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+11:00:00", + "insightsCachedLocale": null, + "cachedPayload": "{not json", + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G08-legacy.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G08-legacy.json.snap new file mode 100644 index 000000000..96cf8ba74 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G08-legacy.json.snap @@ -0,0 +1,62 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Legacy", + "recommendations": [ + "Drink water." + ] + }, + "cached": true, + "cachedAt": "anchor+0d+11:00:00", + "legacyPayload": true, + "revalidating": true, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": true, + "hasProvider": true, + "legacyPayload": true, + "revalidating": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueForceWarm", + "args": [ + { + "userId": "", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+11:00:00", + "insightsCachedLocale": null, + "cachedPayload": { + "summary": "Legacy", + "recommendations": [ + "Drink water." + ] + }, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G09-held-after-failure.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G09-held-after-failure.json.snap new file mode 100644 index 000000000..bb82dddbc --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G09-held-after-failure.json.snap @@ -0,0 +1,102 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+09:00:00" + }, + "cached": true, + "cachedAt": "anchor+0d+09:00:00", + "legacyPayload": false, + "revalidating": false, + "hasProvider": true, + "generationFailed": true, + "generationFailureClass": "rate-limit", + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": true, + "hasProvider": true, + "legacyPayload": false, + "revalidating": false + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": "anchor+0d+09:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+09:00:00" + }, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "reason": "all-providers-failed", + "httpStatus": 429, + "failureKind": null + }, + "ipAddress": null + } + ], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G10-failed-empty.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G10-failed-empty.json.snap new file mode 100644 index 000000000..446fc6618 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G10-failed-empty.json.snap @@ -0,0 +1,71 @@ +{ + "status": 200, + "body": { + "data": { + "insights": null, + "cached": false, + "legacyPayload": false, + "revalidating": true, + "hasProvider": true, + "generationFailed": true, + "generationFailureClass": "format", + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": false, + "hasProvider": true, + "languageMismatch": false, + "readerLocale": "en", + "revalidating": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueForceWarm", + "args": [ + { + "userId": "", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "reason": "provider-error", + "httpStatus": null, + "failureKind": null + }, + "ipAddress": null + }, + { + "action": "insights.briefing-failure", + "details": { + "reason": "invalid-json", + "httpStatus": null, + "failureKind": null + }, + "ipAddress": null + } + ], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G11-withheld.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G11-withheld.json.snap new file mode 100644 index 000000000..82ae9ed31 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G11-withheld.json.snap @@ -0,0 +1,78 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+10:00:00" + }, + "cached": true, + "cachedAt": "anchor+0d+10:00:00", + "legacyPayload": false, + "revalidating": false, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": "ungrounded" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": true, + "hasProvider": true, + "legacyPayload": false, + "revalidating": false + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": "anchor+0d+10:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+10:00:00" + }, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "reason": "briefing-ungrounded", + "httpStatus": null, + "failureKind": null + }, + "ipAddress": null + } + ], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P01-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P01-anonymous.json.snap new file mode 100644 index 000000000..c8614fbac --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P01-anonymous.json.snap @@ -0,0 +1,55 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [], + "stored": { + "insightsCachedAt": "anchor+0d+10:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+10:00:00" + }, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "reason": "briefing-ungrounded", + "httpStatus": null, + "failureKind": null + }, + "ipAddress": null + } + ], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P02-opted-out.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P02-opted-out.json.snap new file mode 100644 index 000000000..b234a3fe3 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P02-opted-out.json.snap @@ -0,0 +1,35 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "This AI feature is turned off in your settings", + "meta": { + "capability": "briefing", + "reason": "user_disabled", + "errorCode": "module.disabled", + "module": "insights", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "AiUnavailableError", + "message": "This AI feature is turned off in your settings" + } + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P03-body-too-large.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P03-body-too-large.json.snap new file mode 100644 index 000000000..83788011a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P03-body-too-large.json.snap @@ -0,0 +1,29 @@ +{ + "status": 413, + "body": { + "data": null, + "error": "Request body exceeds 16384 bytes", + "meta": { + "errorCode": "http.payload_too_large", + "requestId": "" + } + }, + "events": [ + { + "status": 413, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P04-cache-hit.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P04-cache-hit.json.snap new file mode 100644 index 000000000..13f227223 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P04-cache-hit.json.snap @@ -0,0 +1,86 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "cached": true, + "cachedAt": "anchor+0d+11:00:00", + "legacyPayload": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "briefingless": false, + "cached": true, + "legacyPayload": false + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": "anchor+0d+11:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P05-yesterday-regenerated.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P05-yesterday-regenerated.json.snap new file mode 100644 index 000000000..5e7bd73dd --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P05-yesterday-regenerated.json.snap @@ -0,0 +1,276 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "locale": "en", + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P06-other-language-regenerated.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P06-other-language-regenerated.json.snap new file mode 100644 index 000000000..57099b957 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P06-other-language-regenerated.json.snap @@ -0,0 +1,275 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P07-quota-cached-fallback.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P07-quota-cached-fallback.json.snap new file mode 100644 index 000000000..0a33c3fee --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P07-quota-cached-fallback.json.snap @@ -0,0 +1,62 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "cached": true, + "cachedAt": "anchor+0d+11:00:00", + "legacyPayload": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "cached": true, + "locale": "en", + "rate_limited_fallback": true + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": "anchor+0d+11:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P08-quota-refused.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P08-quota-refused.json.snap new file mode 100644 index 000000000..ba22d1ef5 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P08-quota-refused.json.snap @@ -0,0 +1,29 @@ +{ + "status": 429, + "body": { + "data": null, + "error": "Maximum 10 insight generations per hour.", + "meta": { + "errorCode": "http.rate_limited", + "requestId": "" + } + }, + "events": [ + { + "status": 429, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P09-unreadable-key.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P09-unreadable-key.json.snap new file mode 100644 index 000000000..a0924a58b --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P09-unreadable-key.json.snap @@ -0,0 +1,32 @@ +{ + "status": 500, + "body": { + "data": null, + "error": "Interner Serverfehler", + "meta": { + "errorCode": "http.internal", + "requestId": "" + } + }, + "events": [ + { + "status": 500, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "TypeError", + "message": "Invalid authentication tag length: 0" + } + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P10-generated.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P10-generated.json.snap new file mode 100644 index 000000000..3ba9663b0 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P10-generated.json.snap @@ -0,0 +1,275 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 2100 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 2100, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 2100, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P11-strict-reply.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P11-strict-reply.json.snap new file mode 100644 index 000000000..646d8ce72 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P11-strict-reply.json.snap @@ -0,0 +1,251 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "disclaimer": "Consult your doctor.", + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "disclaimer": "Consult your doctor.", + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P12-generated-de-short.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P12-generated-de-short.json.snap new file mode 100644 index 000000000..bc8dbcbf4 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P12-generated-de-short.json.snap @@ -0,0 +1,271 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "off", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_usage_level": "frugal", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000 + }, + "promptChars": 39012, + "promptSha256": "9b74ece939faf8f6643d894cc718bd3e0925133e2c6c12d98dfb3f9d68c82d61" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "de", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "c98e2d8e9f112318293af870eaa7c15d342f7f37a12047b0fd4e2a7f66d84676", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P13-downgraded.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P13-downgraded.json.snap new file mode 100644 index 000000000..1e52a6832 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P13-downgraded.json.snap @@ -0,0 +1,278 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "insights_features_downgraded": true, + "insights_features_hard_downgraded": false, + "insights_features_oversize_bytes": 6100000, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34783, + "promptSha256": "2aeca4de2fc6397815465ac4de27a2267fdfc69cd4a607b2e19d7b06502bc040" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "raw", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P14-hard-downgraded.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P14-hard-downgraded.json.snap new file mode 100644 index 000000000..77bdb63ba --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P14-hard-downgraded.json.snap @@ -0,0 +1,278 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "insights_features_downgraded": true, + "insights_features_hard_downgraded": true, + "insights_features_oversize_bytes": 5400000, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34193, + "promptSha256": "f11bae5de41f1344113977970c832df38427bcdf4b24a6f7fb8b05cef899c5af" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "11a4fb0ae0188e5a95cbe69124afaccdb326b76ea8e8ad73affbc819255a01cc", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "raw", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P15-too-large.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P15-too-large.json.snap new file mode 100644 index 000000000..6f9e66788 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P15-too-large.json.snap @@ -0,0 +1,32 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Your data exceeds the AI payload size limit. Reduce the data window or exclude optional blocks in Settings > AI.", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": { + "insights_payload_oversize_bytes": 5200000, + "insights_payload_too_large": true + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P16-budget-exceeded.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P16-budget-exceeded.json.snap new file mode 100644 index 000000000..63e94ff84 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P16-budget-exceeded.json.snap @@ -0,0 +1,58 @@ +{ + "status": 429, + "body": { + "data": null, + "error": "Daily AI token budget reached. Insights will be available again tomorrow.", + "meta": { + "errorCode": "insights.generate.budgetExceeded", + "requestId": "" + } + }, + "events": [ + { + "status": 429, + "action": { + "name": "insights.briefing.budget_exceeded" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cap": 1600000, + "include_raw": false, + "limit": "owner-cap", + "operatorAfter": 0, + "owner": "user", + "since_days": 400, + "stage": "generate", + "surface": "job", + "totalAfter": 5000000 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 5000000, + "operatorTokens": 0, + "messageCount": 0 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P17-chain-auth.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P17-chain-auth.json.snap new file mode 100644 index 000000000..2b6a467d1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P17-chain-auth.json.snap @@ -0,0 +1,103 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "AI provider rejected the request. Check your API key in Settings > AI", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_chain_attempts": 2, + "insights_chain_first_provider": "anthropic", + "insights_chain_first_status": 401, + "insights_chain_outcome": "all-failed", + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "all-providers-failed", + "locale": "en", + "httpStatus": 401, + "failureKind": null + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "all-providers-failed", + "httpStatus": 401, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P18-chain-rate-limited.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P18-chain-rate-limited.json.snap new file mode 100644 index 000000000..96934ea41 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P18-chain-rate-limited.json.snap @@ -0,0 +1,103 @@ +{ + "status": 429, + "body": { + "data": null, + "error": "AI provider rate-limited the request", + "meta": { + "errorCode": "http.rate_limited", + "requestId": "" + } + }, + "events": [ + { + "status": 429, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_chain_attempts": 1, + "insights_chain_first_provider": "anthropic", + "insights_chain_first_status": 429, + "insights_chain_outcome": "all-failed", + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "all-providers-failed", + "locale": "en", + "httpStatus": 429, + "failureKind": null + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "all-providers-failed", + "httpStatus": 429, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P19-chain-5xx.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P19-chain-5xx.json.snap new file mode 100644 index 000000000..3a50c4531 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P19-chain-5xx.json.snap @@ -0,0 +1,103 @@ +{ + "status": 503, + "body": { + "data": null, + "error": "AI provider temporarily unavailable, try again in a moment", + "meta": { + "errorCode": "http.unavailable", + "requestId": "" + } + }, + "events": [ + { + "status": 503, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_chain_attempts": 2, + "insights_chain_first_provider": "anthropic", + "insights_chain_first_status": 401, + "insights_chain_outcome": "all-failed", + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "all-providers-failed", + "locale": "en", + "httpStatus": 401, + "failureKind": null + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "all-providers-failed", + "httpStatus": 401, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P20-chain-transport.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P20-chain-transport.json.snap new file mode 100644 index 000000000..500f35359 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P20-chain-transport.json.snap @@ -0,0 +1,103 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "AI provider connection failed. Check your AI settings", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_chain_attempts": 2, + "insights_chain_first_provider": "anthropic", + "insights_chain_first_status": null, + "insights_chain_outcome": "all-failed", + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "all-providers-failed", + "locale": "en", + "httpStatus": null, + "failureKind": null + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "all-providers-failed", + "httpStatus": null, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P21-chain-mixed.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P21-chain-mixed.json.snap new file mode 100644 index 000000000..a249fe477 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P21-chain-mixed.json.snap @@ -0,0 +1,103 @@ +{ + "status": 503, + "body": { + "data": null, + "error": "AI provider temporarily unavailable, try again in a moment", + "meta": { + "errorCode": "http.unavailable", + "requestId": "" + } + }, + "events": [ + { + "status": 503, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_chain_attempts": 2, + "insights_chain_first_provider": "anthropic", + "insights_chain_first_status": 404, + "insights_chain_outcome": "all-failed", + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "all-providers-failed", + "locale": "en", + "httpStatus": 404, + "failureKind": null + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "all-providers-failed", + "httpStatus": 404, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-400.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-400.json.snap new file mode 100644 index 000000000..8bd4eb339 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-400.json.snap @@ -0,0 +1,102 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "AI provider connection failed. Check your AI settings", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_provider_body_excerpt": "{\"error\":\"status 400\"}", + "insights_provider_error": "upstream answered 400", + "insights_provider_status": 400, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "provider-error", + "locale": "en", + "httpStatus": 400, + "failureKind": "http" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "provider-error", + "httpStatus": 400, + "failureKind": "http", + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-401.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-401.json.snap new file mode 100644 index 000000000..6175d24a4 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-401.json.snap @@ -0,0 +1,102 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "AI provider rejected the request. Check your API key in Settings > AI", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_provider_body_excerpt": "{\"error\":\"status 401\"}", + "insights_provider_error": "upstream answered 401", + "insights_provider_status": 401, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "provider-error", + "locale": "en", + "httpStatus": 401, + "failureKind": "http" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "provider-error", + "httpStatus": 401, + "failureKind": "http", + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-429.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-429.json.snap new file mode 100644 index 000000000..c220e2f05 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-429.json.snap @@ -0,0 +1,102 @@ +{ + "status": 429, + "body": { + "data": null, + "error": "AI provider rate-limited the request", + "meta": { + "errorCode": "http.rate_limited", + "requestId": "" + } + }, + "events": [ + { + "status": 429, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_provider_body_excerpt": "{\"error\":\"status 429\"}", + "insights_provider_error": "upstream answered 429", + "insights_provider_status": 429, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "provider-error", + "locale": "en", + "httpStatus": 429, + "failureKind": "http" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "provider-error", + "httpStatus": 429, + "failureKind": "http", + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-500.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-500.json.snap new file mode 100644 index 000000000..d8648f6fa --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-500.json.snap @@ -0,0 +1,102 @@ +{ + "status": 503, + "body": { + "data": null, + "error": "AI provider temporarily unavailable, try again in a moment", + "meta": { + "errorCode": "http.unavailable", + "requestId": "" + } + }, + "events": [ + { + "status": 503, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_provider_body_excerpt": "{\"error\":\"status 500\"}", + "insights_provider_error": "upstream answered 500", + "insights_provider_status": 500, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "provider-error", + "locale": "en", + "httpStatus": 500, + "failureKind": "http" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "provider-error", + "httpStatus": 500, + "failureKind": "http", + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P23-invalid-json.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P23-invalid-json.json.snap new file mode 100644 index 000000000..a5781ea92 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P23-invalid-json.json.snap @@ -0,0 +1,97 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "AI response was not valid JSON", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "invalid-json", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "invalid-json", + "httpStatus": null, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P24-truncated.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P24-truncated.json.snap new file mode 100644 index 000000000..77c805581 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P24-truncated.json.snap @@ -0,0 +1,98 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "AI response was cut off before the JSON completed. Raise the token limit (INSIGHTS_MAX_TOKENS)", + "meta": { + "errorCode": "ai_response_truncated", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_response_truncated": true, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "invalid-json", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "invalid-json", + "httpStatus": null, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P25-grounding-retry-kept.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P25-grounding-retry-kept.json.snap new file mode 100644 index 000000000..d02342f18 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P25-grounding-retry-kept.json.snap @@ -0,0 +1,295 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 900, + "ungroundedCount": 1 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000 + }, + "promptChars": 35302, + "promptSha256": "2b3978fcf41077f56835864a67264bea403c0dc31a6bd211ecc469afa16d8a7d" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 900, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 2100, + "operatorTokens": 0, + "messageCount": 2 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P26-grounding-previous.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P26-grounding-previous.json.snap new file mode 100644 index 000000000..39f29a9ce --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P26-grounding-previous.json.snap @@ -0,0 +1,280 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Yesterday's briefing, grounded when it was written.", + "signalsOfDay": [], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "briefing_fallback": "previous-cached", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200, + "ungroundedCount": 1 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000 + }, + "promptChars": 35302, + "promptSha256": "2b3978fcf41077f56835864a67264bea403c0dc31a6bd211ecc469afa16d8a7d" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Yesterday's briefing, grounded when it was written.", + "signalsOfDay": [], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 2400, + "operatorTokens": 0, + "messageCount": 2 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P27-grounding-stripped.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P27-grounding-stripped.json.snap new file mode 100644 index 000000000..6b9b73e81 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P27-grounding-stripped.json.snap @@ -0,0 +1,294 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": "ungrounded" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "briefing_fallback": "stripped", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200, + "ungroundedCount": 1 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000 + }, + "promptChars": 35302, + "promptSha256": "2b3978fcf41077f56835864a67264bea403c0dc31a6bd211ecc469afa16d8a7d" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "briefing-ungrounded", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "briefing-ungrounded", + "httpStatus": null, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + }, + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 2400, + "operatorTokens": 0, + "messageCount": 2 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P28-grounding-retry-unparseable.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P28-grounding-retry-unparseable.json.snap new file mode 100644 index 000000000..6b9b73e81 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P28-grounding-retry-unparseable.json.snap @@ -0,0 +1,294 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": "ungrounded" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "briefing_fallback": "stripped", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200, + "ungroundedCount": 1 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000 + }, + "promptChars": 35302, + "promptSha256": "2b3978fcf41077f56835864a67264bea403c0dc31a6bd211ecc469afa16d8a7d" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "briefing-ungrounded", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "briefing-ungrounded", + "httpStatus": null, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + }, + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 2400, + "operatorTokens": 0, + "messageCount": 2 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P29-outbound-screened.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P29-outbound-screened.json.snap new file mode 100644 index 000000000..9509662c4 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P29-outbound-screened.json.snap @@ -0,0 +1,76 @@ +{ + "status": 502, + "body": { + "data": null, + "error": "The generated insight did not pass the safety check and was discarded. Try regenerating.", + "meta": { + "errorCode": "insights.generate.outboundScreened", + "requestId": "" + } + }, + "events": [ + { + "status": 502, + "action": { + "name": "insights.generate.outbound_blocked" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "locale": "en", + "reason": "risk_score", + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P30-profile-scope-changed.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P30-profile-scope-changed.json.snap new file mode 100644 index 000000000..3f34aaa8d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P30-profile-scope-changed.json.snap @@ -0,0 +1,74 @@ +{ + "status": 409, + "body": { + "data": null, + "error": "Profile scope changed during insight generation", + "meta": { + "errorCode": "insights.generate.profileScopeChanged", + "requestId": "" + } + }, + "events": [ + { + "status": 409, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P31-scope-changed.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P31-scope-changed.json.snap new file mode 100644 index 000000000..a48fd39b2 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P31-scope-changed.json.snap @@ -0,0 +1,74 @@ +{ + "status": 409, + "body": { + "data": null, + "error": "Insight scope changed during generation", + "meta": { + "errorCode": "insights.generate.scopeChanged", + "requestId": "" + } + }, + "events": [ + { + "status": 409, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.01-weight.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.01-weight.json.snap new file mode 100644 index 000000000..13e3bb115 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.01-weight.json.snap @@ -0,0 +1,154 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "weight", + "unit": "kg", + "points": [ + { + "id": "", + "at": "anchor-20d+06:30:00", + "value": 78, + "secondary": null + }, + { + "id": "", + "at": "anchor-19d+06:30:00", + "value": 78.1, + "secondary": null + }, + { + "id": "", + "at": "anchor-18d+06:30:00", + "value": 78.2, + "secondary": null + }, + { + "id": "", + "at": "anchor-17d+06:30:00", + "value": 78.3, + "secondary": null + }, + { + "id": "", + "at": "anchor-16d+06:30:00", + "value": 78.4, + "secondary": null + }, + { + "id": "", + "at": "anchor-15d+06:30:00", + "value": 78.5, + "secondary": null + }, + { + "id": "", + "at": "anchor-13d+06:30:00", + "value": 78.7, + "secondary": null + }, + { + "id": "", + "at": "anchor-12d+06:30:00", + "value": 78.8, + "secondary": null + }, + { + "id": "", + "at": "anchor-11d+06:30:00", + "value": 78.9, + "secondary": null + }, + { + "id": "", + "at": "anchor-10d+06:30:00", + "value": 79, + "secondary": null + }, + { + "id": "", + "at": "anchor-9d+06:30:00", + "value": 79.1, + "secondary": null + }, + { + "id": "", + "at": "anchor-8d+06:30:00", + "value": 79.2, + "secondary": null + }, + { + "id": "", + "at": "anchor-6d+06:30:00", + "value": 79.4, + "secondary": null + }, + { + "id": "", + "at": "anchor-5d+06:30:00", + "value": 79.5, + "secondary": null + }, + { + "id": "", + "at": "anchor-4d+06:30:00", + "value": 79.6, + "secondary": null + }, + { + "id": "", + "at": "anchor-3d+06:30:00", + "value": 79.7, + "secondary": null + }, + { + "id": "", + "at": "anchor-2d+06:30:00", + "value": 79.8, + "secondary": null + }, + { + "id": "", + "at": "anchor-1d+06:30:00", + "value": 79.9, + "secondary": null + } + ], + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "change": { + "from": 78, + "to": 79.9, + "delta": 1.9, + "sentiment": "negative" + }, + "referenceLines": [], + "stats": { + "mean": 78.95, + "min": 78, + "max": 79.9, + "stdDev": 0.6, + "count": 18 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "weight", + "days": 30, + "count": 18 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.02-steps.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.02-steps.json.snap new file mode 100644 index 000000000..745cab7fa --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.02-steps.json.snap @@ -0,0 +1,118 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "steps", + "unit": "steps", + "points": [ + { + "id": "", + "at": "anchor-13d+12:00:00", + "value": 7300, + "secondary": null + }, + { + "id": "", + "at": "anchor-12d+12:00:00", + "value": 7200, + "secondary": null + }, + { + "id": "", + "at": "anchor-11d+12:00:00", + "value": 7100, + "secondary": null + }, + { + "id": "", + "at": "anchor-10d+12:00:00", + "value": 7000, + "secondary": null + }, + { + "id": "", + "at": "anchor-9d+12:00:00", + "value": 6900, + "secondary": null + }, + { + "id": "", + "at": "anchor-8d+12:00:00", + "value": 6800, + "secondary": null + }, + { + "id": "", + "at": "anchor-6d+12:00:00", + "value": 6600, + "secondary": null + }, + { + "id": "", + "at": "anchor-5d+12:00:00", + "value": 6500, + "secondary": null + }, + { + "id": "", + "at": "anchor-4d+12:00:00", + "value": 6400, + "secondary": null + }, + { + "id": "", + "at": "anchor-3d+12:00:00", + "value": 6300, + "secondary": null + }, + { + "id": "", + "at": "anchor-2d+12:00:00", + "value": 6200, + "secondary": null + }, + { + "id": "", + "at": "anchor-1d+12:00:00", + "value": 6100, + "secondary": null + } + ], + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "change": { + "from": 7300, + "to": 6100, + "delta": -1200, + "sentiment": "negative" + }, + "referenceLines": [], + "stats": { + "mean": 6700, + "min": 6100, + "max": 7300, + "stdDev": 389.44, + "count": 12 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "steps", + "days": 14, + "count": 12 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.03-pulse-spread.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.03-pulse-spread.json.snap new file mode 100644 index 000000000..86cf96473 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.03-pulse-spread.json.snap @@ -0,0 +1,190 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "pulse", + "unit": "bpm", + "points": [ + { + "id": "", + "at": "anchor-6d+09:00:00", + "value": 69, + "secondary": null, + "valueMin": 64, + "valueMax": 79 + }, + { + "id": "", + "at": "anchor-6d+14:00:00", + "value": 74, + "secondary": null, + "valueMin": 69, + "valueMax": 84 + }, + { + "id": "", + "at": "anchor-6d+20:00:00", + "value": 80, + "secondary": null, + "valueMin": 75, + "valueMax": 90 + }, + { + "id": "", + "at": "anchor-5d+09:00:00", + "value": 69, + "secondary": null, + "valueMin": 64, + "valueMax": 79 + }, + { + "id": "", + "at": "anchor-5d+14:00:00", + "value": 74, + "secondary": null, + "valueMin": 69, + "valueMax": 84 + }, + { + "id": "", + "at": "anchor-5d+20:00:00", + "value": 80, + "secondary": null, + "valueMin": 75, + "valueMax": 90 + }, + { + "id": "", + "at": "anchor-4d+09:00:00", + "value": 69, + "secondary": null, + "valueMin": 64, + "valueMax": 79 + }, + { + "id": "", + "at": "anchor-4d+14:00:00", + "value": 74, + "secondary": null, + "valueMin": 69, + "valueMax": 84 + }, + { + "id": "", + "at": "anchor-4d+20:00:00", + "value": 80, + "secondary": null, + "valueMin": 75, + "valueMax": 90 + }, + { + "id": "", + "at": "anchor-3d+09:00:00", + "value": 69, + "secondary": null, + "valueMin": 64, + "valueMax": 79 + }, + { + "id": "", + "at": "anchor-3d+14:00:00", + "value": 74, + "secondary": null, + "valueMin": 69, + "valueMax": 84 + }, + { + "id": "", + "at": "anchor-3d+20:00:00", + "value": 80, + "secondary": null, + "valueMin": 75, + "valueMax": 90 + }, + { + "id": "", + "at": "anchor-2d+09:00:00", + "value": 69, + "secondary": null, + "valueMin": 64, + "valueMax": 79 + }, + { + "id": "", + "at": "anchor-2d+14:00:00", + "value": 74, + "secondary": null, + "valueMin": 69, + "valueMax": 84 + }, + { + "id": "", + "at": "anchor-2d+20:00:00", + "value": 80, + "secondary": null, + "valueMin": 75, + "valueMax": 90 + }, + { + "id": "", + "at": "anchor-1d+09:00:00", + "value": 69, + "secondary": null, + "valueMin": 64, + "valueMax": 79 + }, + { + "id": "", + "at": "anchor-1d+14:00:00", + "value": 74, + "secondary": null, + "valueMin": 69, + "valueMax": 84 + }, + { + "id": "", + "at": "anchor-1d+20:00:00", + "value": 80, + "secondary": null, + "valueMin": 75, + "valueMax": 90 + } + ], + "direction": { + "direction": "hold", + "targetPosition": "inside", + "basis": "profile_default" + }, + "change": { + "from": 74.33, + "to": 74.33, + "delta": 0, + "sentiment": "positive" + }, + "referenceLines": [], + "stats": { + "mean": 74.33, + "min": 69, + "max": 80, + "stdDev": 4.5, + "count": 18 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "pulse", + "days": 7, + "count": 18 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.04-blood-pressure.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.04-blood-pressure.json.snap new file mode 100644 index 000000000..f1e9eb005 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.04-blood-pressure.json.snap @@ -0,0 +1,187 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "bloodPressure", + "unit": "mmHg", + "points": [ + { + "id": "", + "at": "anchor-20d+07:01:00", + "value": 126, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-19d+07:01:00", + "value": 130, + "secondary": 82 + }, + { + "id": "", + "at": "anchor-18d+07:01:00", + "value": 129, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-17d+07:01:00", + "value": 128, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-16d+07:01:00", + "value": 127, + "secondary": 82 + }, + { + "id": "", + "at": "anchor-15d+07:01:00", + "value": 126, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-13d+07:01:00", + "value": 129, + "secondary": 82 + }, + { + "id": "", + "at": "anchor-12d+07:01:00", + "value": 128, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-11d+07:01:00", + "value": 127, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-10d+07:01:00", + "value": 126, + "secondary": 82 + }, + { + "id": "", + "at": "anchor-9d+07:01:00", + "value": 130, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-8d+07:01:00", + "value": 129, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-6d+07:01:00", + "value": 127, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-5d+07:01:00", + "value": 126, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-4d+07:01:00", + "value": 130, + "secondary": 82 + }, + { + "id": "", + "at": "anchor-3d+07:01:00", + "value": 129, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-2d+07:01:00", + "value": 128, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-1d+07:01:00", + "value": 127, + "secondary": 82 + } + ], + "direction": { + "direction": "hold", + "targetPosition": "inside", + "basis": "profile_default" + }, + "change": { + "from": 126, + "to": 127, + "delta": 1, + "sentiment": "neutral" + }, + "referenceLines": [ + { + "value": 120, + "kind": "lower", + "source": "personal", + "appliesTo": "value", + "labelKey": "series.reference.targetLower", + "label": "Your target, lower limit" + }, + { + "value": 129, + "kind": "upper", + "source": "personal", + "appliesTo": "value", + "labelKey": "series.reference.targetUpper", + "label": "Your target, upper limit" + }, + { + "value": 70, + "kind": "lower", + "source": "personal", + "appliesTo": "secondary", + "labelKey": "series.reference.targetLower", + "label": "Your target, lower limit" + }, + { + "value": 79, + "kind": "upper", + "source": "personal", + "appliesTo": "secondary", + "labelKey": "series.reference.targetUpper", + "label": "Your target, upper limit" + } + ], + "stats": { + "mean": 127.89, + "min": 126, + "max": 130, + "stdDev": 1.41, + "count": 18 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "bloodPressure", + "days": 30, + "count": 18 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.05-sleep.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.05-sleep.json.snap new file mode 100644 index 000000000..e9adee053 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.05-sleep.json.snap @@ -0,0 +1,190 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "sleep", + "unit": "h", + "points": [ + { + "id": "sleep:day-13", + "at": "anchor-13d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-12", + "at": "anchor-12d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-11", + "at": "anchor-11d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-10", + "at": "anchor-10d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-9", + "at": "anchor-9d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-8", + "at": "anchor-8d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-6", + "at": "anchor-6d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-5", + "at": "anchor-5d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-4", + "at": "anchor-4d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-3", + "at": "anchor-3d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-2", + "at": "anchor-2d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-1", + "at": "anchor-1d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + } + ], + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "change": { + "from": 5.5, + "to": 5.5, + "delta": 0, + "sentiment": "neutral" + }, + "referenceLines": [], + "stats": { + "mean": 5.5, + "min": 5.5, + "max": 5.5, + "stdDev": 0, + "count": 12 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "sleep", + "days": 14, + "count": 12 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.06-glucose-raw.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.06-glucose-raw.json.snap new file mode 100644 index 000000000..f0879d8ac --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.06-glucose-raw.json.snap @@ -0,0 +1,171 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "glucose", + "unit": "mmol/L", + "points": [ + { + "id": "", + "at": "anchor-20d+08:00:00", + "value": 6.4, + "secondary": null + }, + { + "id": "", + "at": "anchor-19d+08:00:00", + "value": 6.3, + "secondary": null + }, + { + "id": "", + "at": "anchor-18d+08:00:00", + "value": 6.3, + "secondary": null + }, + { + "id": "", + "at": "anchor-17d+08:00:00", + "value": 6.2, + "secondary": null + }, + { + "id": "", + "at": "anchor-16d+08:00:00", + "value": 6.2, + "secondary": null + }, + { + "id": "", + "at": "anchor-15d+08:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "", + "at": "anchor-13d+08:00:00", + "value": 6, + "secondary": null + }, + { + "id": "", + "at": "anchor-12d+08:00:00", + "value": 5.9, + "secondary": null + }, + { + "id": "", + "at": "anchor-11d+08:00:00", + "value": 5.9, + "secondary": null + }, + { + "id": "", + "at": "anchor-10d+08:00:00", + "value": 5.8, + "secondary": null + }, + { + "id": "", + "at": "anchor-9d+08:00:00", + "value": 5.8, + "secondary": null + }, + { + "id": "", + "at": "anchor-8d+08:00:00", + "value": 5.7, + "secondary": null + }, + { + "id": "", + "at": "anchor-6d+08:00:00", + "value": 5.6, + "secondary": null + }, + { + "id": "", + "at": "anchor-5d+08:00:00", + "value": 5.5, + "secondary": null + }, + { + "id": "", + "at": "anchor-4d+08:00:00", + "value": 5.5, + "secondary": null + }, + { + "id": "", + "at": "anchor-3d+08:00:00", + "value": 5.4, + "secondary": null + }, + { + "id": "", + "at": "anchor-2d+08:00:00", + "value": 5.4, + "secondary": null + }, + { + "id": "", + "at": "anchor-1d+08:00:00", + "value": 5.3, + "secondary": null + } + ], + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "change": { + "from": 6.4, + "to": 5.3, + "delta": -1.1, + "sentiment": "positive" + }, + "referenceLines": [ + { + "value": 3.9, + "kind": "lower", + "source": "clinical", + "appliesTo": "value", + "labelKey": "series.reference.clinicalLower", + "label": "Clinical lower limit" + }, + { + "value": 10, + "kind": "upper", + "source": "clinical", + "appliesTo": "value", + "labelKey": "series.reference.clinicalUpper", + "label": "Clinical upper limit" + } + ], + "stats": { + "mean": 5.9, + "min": 5.3, + "max": 6.4, + "stdDev": 0.3, + "count": 18 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "glucose", + "days": 30, + "count": 18 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.07-glucose-long.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.07-glucose-long.json.snap new file mode 100644 index 000000000..e33221200 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.07-glucose-long.json.snap @@ -0,0 +1,237 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "glucose", + "unit": "mmol/L", + "points": [ + { + "id": "day:day-70", + "at": "anchor-70d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-69", + "at": "anchor-69d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-68", + "at": "anchor-68d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-67", + "at": "anchor-67d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-66", + "at": "anchor-66d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-65", + "at": "anchor-65d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-64", + "at": "anchor-64d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-63", + "at": "anchor-63d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-62", + "at": "anchor-62d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-61", + "at": "anchor-61d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-60", + "at": "anchor-60d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-20", + "at": "anchor-20d+00:00:00", + "value": 6.4, + "secondary": null + }, + { + "id": "day:day-19", + "at": "anchor-19d+00:00:00", + "value": 6.3, + "secondary": null + }, + { + "id": "day:day-18", + "at": "anchor-18d+00:00:00", + "value": 6.3, + "secondary": null + }, + { + "id": "day:day-17", + "at": "anchor-17d+00:00:00", + "value": 6.2, + "secondary": null + }, + { + "id": "day:day-16", + "at": "anchor-16d+00:00:00", + "value": 6.2, + "secondary": null + }, + { + "id": "day:day-15", + "at": "anchor-15d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-13", + "at": "anchor-13d+00:00:00", + "value": 6, + "secondary": null + }, + { + "id": "day:day-12", + "at": "anchor-12d+00:00:00", + "value": 5.9, + "secondary": null + }, + { + "id": "day:day-11", + "at": "anchor-11d+00:00:00", + "value": 5.9, + "secondary": null + }, + { + "id": "day:day-10", + "at": "anchor-10d+00:00:00", + "value": 5.8, + "secondary": null + }, + { + "id": "day:day-9", + "at": "anchor-9d+00:00:00", + "value": 5.8, + "secondary": null + }, + { + "id": "day:day-8", + "at": "anchor-8d+00:00:00", + "value": 5.7, + "secondary": null + }, + { + "id": "day:day-6", + "at": "anchor-6d+00:00:00", + "value": 5.6, + "secondary": null + }, + { + "id": "day:day-5", + "at": "anchor-5d+00:00:00", + "value": 5.5, + "secondary": null + }, + { + "id": "day:day-4", + "at": "anchor-4d+00:00:00", + "value": 5.5, + "secondary": null + }, + { + "id": "day:day-3", + "at": "anchor-3d+00:00:00", + "value": 5.4, + "secondary": null + }, + { + "id": "day:day-2", + "at": "anchor-2d+00:00:00", + "value": 5.4, + "secondary": null + }, + { + "id": "day:day-1", + "at": "anchor-1d+00:00:00", + "value": 5.3, + "secondary": null + } + ], + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "change": { + "from": 6.1, + "to": 5.3, + "delta": -0.8, + "sentiment": "positive" + }, + "referenceLines": [ + { + "value": 3.9, + "kind": "lower", + "source": "clinical", + "appliesTo": "value", + "labelKey": "series.reference.clinicalLower", + "label": "Clinical lower limit" + }, + { + "value": 10, + "kind": "upper", + "source": "clinical", + "appliesTo": "value", + "labelKey": "series.reference.clinicalUpper", + "label": "Clinical upper limit" + } + ], + "stats": { + "mean": 6, + "min": 5.3, + "max": 6.4, + "stdDev": 0.3, + "count": 29 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "glucose", + "days": 365, + "count": 29 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.08-hrv-hour-buckets.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.08-hrv-hour-buckets.json.snap new file mode 100644 index 000000000..a3d19aec3 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.08-hrv-hour-buckets.json.snap @@ -0,0 +1,1066 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "heartRateVariability", + "unit": "ms", + "points": [ + { + "id": "hour:anchor-8d+22:00:00", + "at": "anchor-8d+22:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-8d+23:00:00", + "at": "anchor-8d+23:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-7d+00:00:00", + "at": "anchor-7d+00:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-7d+01:00:00", + "at": "anchor-7d+01:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-7d+02:00:00", + "at": "anchor-7d+02:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-7d+03:00:00", + "at": "anchor-7d+03:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-7d+04:00:00", + "at": "anchor-7d+04:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-7d+05:00:00", + "at": "anchor-7d+05:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-7d+06:00:00", + "at": "anchor-7d+06:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-7d+07:00:00", + "at": "anchor-7d+07:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-7d+08:00:00", + "at": "anchor-7d+08:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-7d+09:00:00", + "at": "anchor-7d+09:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-7d+10:00:00", + "at": "anchor-7d+10:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-7d+11:00:00", + "at": "anchor-7d+11:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-7d+12:00:00", + "at": "anchor-7d+12:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-7d+13:00:00", + "at": "anchor-7d+13:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-7d+14:00:00", + "at": "anchor-7d+14:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-7d+15:00:00", + "at": "anchor-7d+15:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-7d+16:00:00", + "at": "anchor-7d+16:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-7d+17:00:00", + "at": "anchor-7d+17:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-7d+18:00:00", + "at": "anchor-7d+18:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-7d+19:00:00", + "at": "anchor-7d+19:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-7d+20:00:00", + "at": "anchor-7d+20:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-7d+21:00:00", + "at": "anchor-7d+21:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-7d+22:00:00", + "at": "anchor-7d+22:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-7d+23:00:00", + "at": "anchor-7d+23:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-6d+00:00:00", + "at": "anchor-6d+00:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-6d+01:00:00", + "at": "anchor-6d+01:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-6d+02:00:00", + "at": "anchor-6d+02:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-6d+03:00:00", + "at": "anchor-6d+03:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-6d+04:00:00", + "at": "anchor-6d+04:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-6d+05:00:00", + "at": "anchor-6d+05:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-6d+06:00:00", + "at": "anchor-6d+06:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-6d+07:00:00", + "at": "anchor-6d+07:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-6d+08:00:00", + "at": "anchor-6d+08:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-6d+09:00:00", + "at": "anchor-6d+09:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-6d+10:00:00", + "at": "anchor-6d+10:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-6d+11:00:00", + "at": "anchor-6d+11:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-6d+12:00:00", + "at": "anchor-6d+12:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-6d+13:00:00", + "at": "anchor-6d+13:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-6d+14:00:00", + "at": "anchor-6d+14:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-6d+15:00:00", + "at": "anchor-6d+15:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-6d+16:00:00", + "at": "anchor-6d+16:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-6d+17:00:00", + "at": "anchor-6d+17:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-6d+18:00:00", + "at": "anchor-6d+18:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-6d+19:00:00", + "at": "anchor-6d+19:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-6d+20:00:00", + "at": "anchor-6d+20:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-6d+21:00:00", + "at": "anchor-6d+21:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-6d+22:00:00", + "at": "anchor-6d+22:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-6d+23:00:00", + "at": "anchor-6d+23:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-5d+00:00:00", + "at": "anchor-5d+00:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-5d+01:00:00", + "at": "anchor-5d+01:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-5d+02:00:00", + "at": "anchor-5d+02:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-5d+03:00:00", + "at": "anchor-5d+03:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-5d+04:00:00", + "at": "anchor-5d+04:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-5d+05:00:00", + "at": "anchor-5d+05:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-5d+06:00:00", + "at": "anchor-5d+06:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-5d+07:00:00", + "at": "anchor-5d+07:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-5d+08:00:00", + "at": "anchor-5d+08:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-5d+09:00:00", + "at": "anchor-5d+09:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-5d+10:00:00", + "at": "anchor-5d+10:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-5d+11:00:00", + "at": "anchor-5d+11:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-5d+12:00:00", + "at": "anchor-5d+12:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-5d+13:00:00", + "at": "anchor-5d+13:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-5d+14:00:00", + "at": "anchor-5d+14:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-5d+15:00:00", + "at": "anchor-5d+15:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-5d+16:00:00", + "at": "anchor-5d+16:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-5d+17:00:00", + "at": "anchor-5d+17:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-5d+18:00:00", + "at": "anchor-5d+18:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-5d+19:00:00", + "at": "anchor-5d+19:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-5d+20:00:00", + "at": "anchor-5d+20:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-5d+21:00:00", + "at": "anchor-5d+21:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-5d+22:00:00", + "at": "anchor-5d+22:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-5d+23:00:00", + "at": "anchor-5d+23:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-4d+00:00:00", + "at": "anchor-4d+00:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-4d+01:00:00", + "at": "anchor-4d+01:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-4d+02:00:00", + "at": "anchor-4d+02:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-4d+03:00:00", + "at": "anchor-4d+03:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-4d+04:00:00", + "at": "anchor-4d+04:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-4d+05:00:00", + "at": "anchor-4d+05:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-4d+06:00:00", + "at": "anchor-4d+06:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-4d+07:00:00", + "at": "anchor-4d+07:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-4d+08:00:00", + "at": "anchor-4d+08:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-4d+09:00:00", + "at": "anchor-4d+09:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-4d+10:00:00", + "at": "anchor-4d+10:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-4d+11:00:00", + "at": "anchor-4d+11:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-4d+12:00:00", + "at": "anchor-4d+12:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-4d+13:00:00", + "at": "anchor-4d+13:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-4d+14:00:00", + "at": "anchor-4d+14:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-4d+15:00:00", + "at": "anchor-4d+15:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-4d+16:00:00", + "at": "anchor-4d+16:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-4d+17:00:00", + "at": "anchor-4d+17:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-4d+18:00:00", + "at": "anchor-4d+18:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-4d+19:00:00", + "at": "anchor-4d+19:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-4d+20:00:00", + "at": "anchor-4d+20:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-4d+21:00:00", + "at": "anchor-4d+21:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-4d+22:00:00", + "at": "anchor-4d+22:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-4d+23:00:00", + "at": "anchor-4d+23:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-3d+00:00:00", + "at": "anchor-3d+00:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-3d+01:00:00", + "at": "anchor-3d+01:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-3d+02:00:00", + "at": "anchor-3d+02:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-3d+03:00:00", + "at": "anchor-3d+03:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-3d+04:00:00", + "at": "anchor-3d+04:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-3d+05:00:00", + "at": "anchor-3d+05:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-3d+06:00:00", + "at": "anchor-3d+06:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-3d+07:00:00", + "at": "anchor-3d+07:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-3d+08:00:00", + "at": "anchor-3d+08:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-3d+09:00:00", + "at": "anchor-3d+09:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-3d+10:00:00", + "at": "anchor-3d+10:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-3d+11:00:00", + "at": "anchor-3d+11:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-3d+12:00:00", + "at": "anchor-3d+12:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-3d+13:00:00", + "at": "anchor-3d+13:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-3d+14:00:00", + "at": "anchor-3d+14:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-3d+15:00:00", + "at": "anchor-3d+15:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-3d+16:00:00", + "at": "anchor-3d+16:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-3d+17:00:00", + "at": "anchor-3d+17:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-3d+18:00:00", + "at": "anchor-3d+18:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-3d+19:00:00", + "at": "anchor-3d+19:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-3d+20:00:00", + "at": "anchor-3d+20:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-3d+21:00:00", + "at": "anchor-3d+21:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-3d+22:00:00", + "at": "anchor-3d+22:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-3d+23:00:00", + "at": "anchor-3d+23:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-2d+00:00:00", + "at": "anchor-2d+00:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-2d+01:00:00", + "at": "anchor-2d+01:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-2d+02:00:00", + "at": "anchor-2d+02:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-2d+03:00:00", + "at": "anchor-2d+03:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-2d+04:00:00", + "at": "anchor-2d+04:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-2d+05:00:00", + "at": "anchor-2d+05:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-2d+06:00:00", + "at": "anchor-2d+06:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-2d+07:00:00", + "at": "anchor-2d+07:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-2d+08:00:00", + "at": "anchor-2d+08:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-2d+09:00:00", + "at": "anchor-2d+09:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-2d+10:00:00", + "at": "anchor-2d+10:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-2d+11:00:00", + "at": "anchor-2d+11:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-2d+12:00:00", + "at": "anchor-2d+12:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-2d+13:00:00", + "at": "anchor-2d+13:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-2d+14:00:00", + "at": "anchor-2d+14:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-2d+15:00:00", + "at": "anchor-2d+15:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-2d+16:00:00", + "at": "anchor-2d+16:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-2d+17:00:00", + "at": "anchor-2d+17:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-2d+18:00:00", + "at": "anchor-2d+18:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-2d+19:00:00", + "at": "anchor-2d+19:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-2d+20:00:00", + "at": "anchor-2d+20:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-2d+21:00:00", + "at": "anchor-2d+21:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-2d+22:00:00", + "at": "anchor-2d+22:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-2d+23:00:00", + "at": "anchor-2d+23:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-1d+00:00:00", + "at": "anchor-1d+00:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-1d+01:00:00", + "at": "anchor-1d+01:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-1d+02:00:00", + "at": "anchor-1d+02:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-1d+03:00:00", + "at": "anchor-1d+03:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-1d+04:00:00", + "at": "anchor-1d+04:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-1d+05:00:00", + "at": "anchor-1d+05:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-1d+06:00:00", + "at": "anchor-1d+06:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-1d+07:00:00", + "at": "anchor-1d+07:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-1d+08:00:00", + "at": "anchor-1d+08:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-1d+09:00:00", + "at": "anchor-1d+09:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-1d+10:00:00", + "at": "anchor-1d+10:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-1d+11:00:00", + "at": "anchor-1d+11:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-1d+12:00:00", + "at": "anchor-1d+12:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-1d+13:00:00", + "at": "anchor-1d+13:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-1d+14:00:00", + "at": "anchor-1d+14:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-1d+15:00:00", + "at": "anchor-1d+15:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-1d+16:00:00", + "at": "anchor-1d+16:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-1d+17:00:00", + "at": "anchor-1d+17:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-1d+18:00:00", + "at": "anchor-1d+18:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-1d+19:00:00", + "at": "anchor-1d+19:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-1d+20:00:00", + "at": "anchor-1d+20:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-1d+21:00:00", + "at": "anchor-1d+21:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-1d+22:00:00", + "at": "anchor-1d+22:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-1d+23:00:00", + "at": "anchor-1d+23:00:00", + "value": 47.4, + "secondary": null + } + ], + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "change": { + "from": 48.07, + "to": 47.98, + "delta": -0.09, + "sentiment": "negative" + }, + "referenceLines": [], + "stats": { + "mean": 48, + "min": 40, + "max": 56, + "stdDev": 4.9, + "count": 10200 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "heartRateVariability", + "days": 30, + "count": 170 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.09-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.09-invalid.json.snap new file mode 100644 index 000000000..4287a32ba --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.09-invalid.json.snap @@ -0,0 +1,56 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Validation failed", + "details": { + "issues": [ + { + "path": "kind", + "code": "invalid_value", + "message": "Invalid option: expected one of \"weight\"|\"bloodPressure\"|\"pulse\"|\"bodyFat\"|\"glucose\"|\"sleep\"|\"steps\"|\"totalBodyWater\"|\"boneMass\"|\"oxygenSaturation\"|\"restingHeartRate\"|\"heartRateVariability\"|\"vo2Max\"" + }, + { + "path": "days", + "code": "too_small", + "message": "Too small: expected number to be >=1" + } + ] + }, + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "measurements.series.validation-failed" + }, + "meta": { + "issue_count": 2, + "received_keys": [ + "kind", + "days" + ], + "received_shape_excerpt": "{\"kind\":\"temperature\",\"days\":\"0\"}", + "zod_issues": [ + { + "path": "kind", + "code": "invalid_value", + "message": "Invalid option: expected one of \"weight\"|\"bloodPressure\"|\"pulse\"|\"bodyFat\"|\"glucose\"|\"sleep\"|\"steps\"|\"totalBodyWater\"|\"boneMass\"|\"oxygenSaturation\"|\"restingHeartRate\"|\"heartRateVariability\"|\"vo2Max\"" + }, + { + "path": "days", + "code": "too_small", + "message": "Too small: expected number to be >=1" + } + ], + "rejected_attempt": "measurements.series.validation-failed" + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.10-delegated.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.10-delegated.json.snap new file mode 100644 index 000000000..200db4ea9 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.10-delegated.json.snap @@ -0,0 +1,115 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "bloodPressure", + "unit": "mmHg", + "points": [ + { + "id": "", + "at": "anchor-6d+07:01:00", + "value": 127, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-5d+07:01:00", + "value": 126, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-4d+07:01:00", + "value": 130, + "secondary": 82 + }, + { + "id": "", + "at": "anchor-3d+07:01:00", + "value": 129, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-2d+07:01:00", + "value": 128, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-1d+07:01:00", + "value": 127, + "secondary": 82 + } + ], + "direction": { + "direction": "hold", + "targetPosition": "inside", + "basis": "profile_default" + }, + "change": { + "from": 127, + "to": 127, + "delta": 0, + "sentiment": "positive" + }, + "referenceLines": [ + { + "value": 120, + "kind": "lower", + "source": "personal", + "appliesTo": "value", + "labelKey": "series.reference.targetLower", + "label": "Dein Ziel, Untergrenze" + }, + { + "value": 129, + "kind": "upper", + "source": "personal", + "appliesTo": "value", + "labelKey": "series.reference.targetUpper", + "label": "Dein Ziel, Obergrenze" + }, + { + "value": 70, + "kind": "lower", + "source": "personal", + "appliesTo": "secondary", + "labelKey": "series.reference.targetLower", + "label": "Dein Ziel, Untergrenze" + }, + { + "value": 79, + "kind": "upper", + "source": "personal", + "appliesTo": "secondary", + "labelKey": "series.reference.targetUpper", + "label": "Dein Ziel, Obergrenze" + } + ], + "stats": { + "mean": 127.83, + "min": 126, + "max": 130, + "stdDev": 1.34, + "count": 6 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "bloodPressure", + "days": 7, + "count": 6 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurements.get-05-drill-down.json.snap b/tests/integration/routes/parity/__snapshots__/measurements.get-05-drill-down.json.snap index 4e6691c32..9867be855 100644 --- a/tests/integration/routes/parity/__snapshots__/measurements.get-05-drill-down.json.snap +++ b/tests/integration/routes/parity/__snapshots__/measurements.get-05-drill-down.json.snap @@ -14,7 +14,6 @@ "source": "APPLE_HEALTH", "measuredAt": "2026-10-13T12:00:00.000Z", "notes": null, - "notesEncrypted": null, "externalId": "stats:HKQuantityTypeIdentifierStepCount:2026-10-13", "externalSourceVersion": null, "aggregationProvenance": "HEALTHKIT_STATISTICS", diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.01-rolling.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.01-rolling.json.snap new file mode 100644 index 000000000..7d5a09722 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.01-rolling.json.snap @@ -0,0 +1,699 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 1 + } + ], + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "categoryLabel": "Heart and circulation", + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false, + "nextDueAt": "2026-10-17T07:00:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": 3, + "stockDosesRemaining": 3, + "runwayDays": 21, + "ingredients": [ + { + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "labelKey": "nutrients.names.vitamin_d", + "label": "Vitamin D" + } + ], + "related": [] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.get", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.02-daily.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.02-daily.json.snap new file mode 100644 index 000000000..618a09d7a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.02-daily.json.snap @@ -0,0 +1,710 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Daily tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 1 + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false, + "nextDueAt": "2026-10-15T18:00:00.000Z", + "remindersActive": true, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.get", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.03-record-only.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.03-record-only.json.snap new file mode 100644 index 000000000..9b038c771 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.03-record-only.json.snap @@ -0,0 +1,690 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [], + "recordedSchedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false, + "nextDueAt": null, + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": false, + "stockUnitsRemaining": null, + "stockDosesRemaining": null, + "runwayDays": null, + "ingredients": [], + "related": [] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.get", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.04-not-found.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.04-not-found.json.snap new file mode 100644 index 000000000..4be62bf07 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.04-not-found.json.snap @@ -0,0 +1,617 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Medication not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.05a-reader.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.05a-reader.json.snap new file mode 100644 index 000000000..7d5a09722 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.05a-reader.json.snap @@ -0,0 +1,699 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 1 + } + ], + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "categoryLabel": "Heart and circulation", + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false, + "nextDueAt": "2026-10-17T07:00:00.000Z", + "remindersActive": false, + "nextDueOverdue": false, + "courseStatus": "CURRENT", + "intakeActionable": true, + "stockUnitsRemaining": 3, + "stockDosesRemaining": 3, + "runwayDays": 21, + "ingredients": [ + { + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "labelKey": "nutrients.names.vitamin_d", + "label": "Vitamin D" + } + ], + "related": [] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.get", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.05b-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.05b-anonymous.json.snap new file mode 100644 index 000000000..408d19cc6 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.05b-anonymous.json.snap @@ -0,0 +1,620 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.11-plain-fields.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.11-plain-fields.json.snap new file mode 100644 index 000000000..caac60735 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.11-plain-fields.json.snap @@ -0,0 +1,718 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 1 + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.12-schedule-replace.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.12-schedule-replace.json.snap new file mode 100644 index 000000000..9253f75d6 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.12-schedule-replace.json.snap @@ -0,0 +1,794 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": 2, + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 30, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 2 + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ], + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": { + "schedule_revision_rows": 2 + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 30, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.13-schedule-echo.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.13-schedule-echo.json.snap new file mode 100644 index 000000000..76cbe084b --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.13-schedule-echo.json.snap @@ -0,0 +1,808 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": 2, + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 30, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 2 + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ], + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 30, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.14-grace-bridge.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.14-grace-bridge.json.snap new file mode 100644 index 000000000..9592315ae --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.14-grace-bridge.json.snap @@ -0,0 +1,819 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": 2, + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 2 + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ], + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.15a-pause.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.15a-pause.json.snap new file mode 100644 index 000000000..58c4631c6 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.15a-pause.json.snap @@ -0,0 +1,843 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": 1, + "active": false, + "notificationsEnabled": false, + "pausedAt": "", + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": 2, + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 2 + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ], + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": false, + "notificationsEnabled": false, + "pausedAt": "", + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": null, + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.15b-resume.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.15b-resume.json.snap new file mode 100644 index 000000000..80d5f956d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.15b-resume.json.snap @@ -0,0 +1,858 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": 2, + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 2 + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ], + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.16-auto-mark-taken.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.16-auto-mark-taken.json.snap new file mode 100644 index 000000000..f960f6d45 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.16-auto-mark-taken.json.snap @@ -0,0 +1,873 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": 2, + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 2 + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ], + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.17a-custom-category.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.17a-custom-category.json.snap new file mode 100644 index 000000000..41c112d3d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.17a-custom-category.json.snap @@ -0,0 +1,889 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": 2, + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 2 + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ], + "resolvedUnitsPerDose": 1 + } + ], + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "categoryLabel": "Heart and circulation", + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.17b-native-other-kept.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.17b-native-other-kept.json.snap new file mode 100644 index 000000000..e537872e2 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.17b-native-other-kept.json.snap @@ -0,0 +1,902 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": 2, + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 2 + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ], + "resolvedUnitsPerDose": 1 + } + ], + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "categoryLabel": "Heart and circulation", + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": { + "entity_id": "" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.17c-web-other.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.17c-web-other.json.snap new file mode 100644 index 000000000..aa42bd9a1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.17c-web-other.json.snap @@ -0,0 +1,911 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": 2, + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 2 + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ], + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.17d-unknown-category.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.17d-unknown-category.json.snap new file mode 100644 index 000000000..42df8b7a2 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.17d-unknown-category.json.snap @@ -0,0 +1,822 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Unknown medication category", + "meta": { + "errorCode": "medications.category.unknown", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.18a-record-schedule-kept.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.18a-record-schedule-kept.json.snap new file mode 100644 index 000000000..812cf0903 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.18a-record-schedule-kept.json.snap @@ -0,0 +1,904 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [], + "recordedSchedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": { + "dropped_schedules": true, + "dropped_as_needed": false, + "dropped_one_shot": true + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.18b-tracking-on.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.18b-tracking-on.json.snap new file mode 100644 index 000000000..152c79133 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.18b-tracking-on.json.snap @@ -0,0 +1,927 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": { + "schedule_revision_rows": 0, + "track_intake": true + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.18c-tracking-off.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.18c-tracking-off.json.snap new file mode 100644 index 000000000..160f86063 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.18c-tracking-off.json.snap @@ -0,0 +1,971 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [], + "recordedSchedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": { + "schedule_revision_rows": 1, + "track_intake": false + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.19a-scheduled-without-schedules.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.19a-scheduled-without-schedules.json.snap new file mode 100644 index 000000000..a114b01cb --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.19a-scheduled-without-schedules.json.snap @@ -0,0 +1,905 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "A scheduled medication requires at least one schedule", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.19b-scheduled.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.19b-scheduled.json.snap new file mode 100644 index 000000000..18bea73e4 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.19b-scheduled.json.snap @@ -0,0 +1,1019 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": { + "schedule_revision_rows": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.19c-as-needed-keeping-schedules.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.19c-as-needed-keeping-schedules.json.snap new file mode 100644 index 000000000..fbcedbd50 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.19c-as-needed-keeping-schedules.json.snap @@ -0,0 +1,955 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "An as-needed medication cannot carry schedules (send schedules: [] to clear them)", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.19d-as-needed.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.19d-as-needed.json.snap new file mode 100644 index 000000000..f1c85281a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.19d-as-needed.json.snap @@ -0,0 +1,1025 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": { + "schedule_revision_rows": 1 + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.20a-one-shot-two-schedules.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.20a-one-shot-two-schedules.json.snap new file mode 100644 index 000000000..bf46e1b89 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.20a-one-shot-two-schedules.json.snap @@ -0,0 +1,983 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "A one-shot medication can have at most one schedule", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.20b-one-shot-recurrence.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.20b-one-shot-recurrence.json.snap new file mode 100644 index 000000000..a7639f5bc --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.20b-one-shot-recurrence.json.snap @@ -0,0 +1,983 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "A one-shot medication cannot have a recurrence (rrule or rollingIntervalDays)", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.20c-one-shot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.20c-one-shot.json.snap new file mode 100644 index 000000000..7ca869539 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.20c-one-shot.json.snap @@ -0,0 +1,1114 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [ + { + "id": "", + "startsOn": "2026-10-20", + "endsOn": "2026-10-20", + "status": "UPCOMING", + "takenDoses": 0, + "note": null + } + ], + "courseCount": 1, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": { + "schedule_revision_rows": 1 + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.21a-course-overlap.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.21a-course-overlap.json.snap new file mode 100644 index 000000000..95ee616b8 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.21a-course-overlap.json.snap @@ -0,0 +1,1041 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Courses may not share a day", + "meta": { + "errorCode": "medications.course.overlap", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.21b-course-window-required.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.21b-course-window-required.json.snap new file mode 100644 index 000000000..29c9db892 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.21b-course-window-required.json.snap @@ -0,0 +1,1041 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "A medication with several courses keeps a start date; delete a course instead", + "meta": { + "errorCode": "medications.course.windowRequired", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.21c-course-end.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.21c-course-end.json.snap new file mode 100644 index 000000000..e89aacbc9 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.21c-course-end.json.snap @@ -0,0 +1,1135 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [ + { + "id": "", + "startsOn": "2026-08-01", + "endsOn": "2026-08-31", + "status": "ENDED", + "takenDoses": 0, + "note": null + }, + { + "id": "", + "startsOn": "2026-10-01", + "endsOn": "2026-12-31", + "status": "CURRENT", + "takenDoses": 0, + "note": null + } + ], + "courseCount": 2, + "previousCourseEndedOn": "2026-08-31", + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.22-composition-units.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.22-composition-units.json.snap new file mode 100644 index 000000000..50d79355e --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.22-composition-units.json.snap @@ -0,0 +1,1136 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": 2, + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 2 + } + ], + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "categoryLabel": "Heart and circulation", + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueSupplementNutrientRecompute", + "args": [ + "" + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "2", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.23a-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.23a-invalid.json.snap new file mode 100644 index 000000000..0967c7eaa --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.23a-invalid.json.snap @@ -0,0 +1,1086 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Validation failed", + "details": { + "issues": [ + { + "path": "name", + "code": "too_small", + "message": "Too small: expected string to have >=1 characters" + }, + { + "path": "unitsPerDose", + "code": "custom", + "message": "unitsPerDose must be above 0 and at most 100, with at most 4 decimal places (e.g. 1, 0.5, 1.5, 2.25)" + }, + { + "path": "schedules", + "code": "invalid_type", + "message": "Invalid input: expected array, received string" + } + ] + }, + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "2", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.23b-not-found.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.23b-not-found.json.snap new file mode 100644 index 000000000..9848e8212 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.23b-not-found.json.snap @@ -0,0 +1,1067 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Medication not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "2", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.24a-writer.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.24a-writer.json.snap new file mode 100644 index 000000000..28c205ead --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.24a-writer.json.snap @@ -0,0 +1,1072 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "Account access denied", + "meta": { + "errorCode": "sharing.access.denied", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": null, + "meta": { + "sharing_refusal": "insufficient_access" + }, + "warnings": null, + "error": { + "type": "SharingAccessDeniedError", + "message": "Account access denied" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Daily tablet renamed", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "2", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.24b-manager.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.24b-manager.json.snap new file mode 100644 index 000000000..46f368ca1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.24b-manager.json.snap @@ -0,0 +1,1171 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "name": "By a manager", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": 1, + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "", + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": 2, + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null, + "resolvedUnitsPerDose": 2 + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ], + "resolvedUnitsPerDose": 1 + } + ], + "category": "OTHER", + "categoryLabel": null, + "courses": [], + "courseCount": 0, + "previousCourseEndedOn": null, + "canStartCourse": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.update", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "By a manager", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "2", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Daily tablet renamed" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.24c-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.24c-anonymous.json.snap new file mode 100644 index 000000000..028da3263 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.24c-anonymous.json.snap @@ -0,0 +1,1085 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "By a manager", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "2", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "To delete", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "12:00", + "windowEnd": "13:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "12:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "magnesium", + "amountPerUnit": 100, + "unit": "mg", + "enteredAmount": null, + "enteredUnit": null, + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.update", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Daily tablet renamed" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.31-delete.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.31-delete.json.snap new file mode 100644 index 000000000..4459480a0 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.31-delete.json.snap @@ -0,0 +1,1050 @@ +{ + "status": 200, + "body": { + "data": { + "deleted": true + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.delete", + "entity_type": "medication", + "entity_id": "" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueSupplementNutrientRecompute", + "args": [ + "" + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "By a manager", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "2", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.delete", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "name": "To delete" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Daily tablet renamed" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": [ + { + "name": "kit-ingest", + "permissions": [ + "medication::ingest" + ], + "revoked": true + }, + { + "name": "kit-phone", + "permissions": [ + "*" + ], + "revoked": false + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.32-not-found.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.32-not-found.json.snap new file mode 100644 index 000000000..5f7a74b90 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.32-not-found.json.snap @@ -0,0 +1,1041 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Medication not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "By a manager", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "2", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.delete", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "name": "To delete" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Daily tablet renamed" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": [ + { + "name": "kit-ingest", + "permissions": [ + "medication::ingest" + ], + "revoked": true + }, + { + "name": "kit-phone", + "permissions": [ + "*" + ], + "revoked": false + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.33a-manager.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.33a-manager.json.snap new file mode 100644 index 000000000..849675a62 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.33a-manager.json.snap @@ -0,0 +1,1046 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "This endpoint cannot be used while acting on another account", + "meta": { + "errorCode": "sharing.not_permitted", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": null, + "meta": { + "sharing_refusal": "undeclared_mode" + }, + "warnings": null, + "error": { + "type": "SharingNotPermittedError", + "message": "This endpoint cannot be used while acting on another account" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "By a manager", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "2", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.delete", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "name": "To delete" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Daily tablet renamed" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": [ + { + "name": "kit-ingest", + "permissions": [ + "medication::ingest" + ], + "revoked": true + }, + { + "name": "kit-phone", + "permissions": [ + "*" + ], + "revoked": false + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-detail.33b-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-detail.33b-anonymous.json.snap new file mode 100644 index 000000000..29eef6fb4 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-detail.33b-anonymous.json.snap @@ -0,0 +1,1044 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Booster", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "By a manager", + "dose": "20mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": 30, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": false, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "", + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": true, + "criticalAlarmEnabled": true, + "atcCode": "C09AA05", + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": 5, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Course tablet", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only renamed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Rolling pen", + "dose": "1 pen", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "2", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": true, + "autoMarkTakenSince": "2026-10-01T06:00:00.000Z", + "deliveryForm": "INJECTION", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "09:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": 7, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "11:30", + "windowEnd": "11:30", + "label": "Late morning", + "dose": "10mg", + "unitsPerDose": "2", + "daysOfWeek": null, + "timesOfDay": [ + "11:30" + ], + "reminderGraceMinutes": 45, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": "i2;1,3,5", + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": [ + { + "end": "21:00", + "start": "19:30", + "timeOfDay": "20:00" + } + ] + } + ], + "revisions": [ + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + }, + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "22:00", + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "doseWindows": null, + "windowStart": "20:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "2026-10-01T06:00:00.000Z", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "10:00", + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "doseWindows": null, + "windowStart": "09:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "08:00", + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "doseWindows": null, + "windowStart": "07:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + }, + { + "id": "", + "medicationId": "", + "validFrom": "", + "validUntil": "", + "payload": [ + { + "dose": null, + "label": null, + "rrule": "FREQ=DAILY", + "windowEnd": "09:00", + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "doseWindows": null, + "windowStart": "08:00", + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "rollingIntervalDays": null, + "reminderGraceMinutes": null + } + ], + "source": "ARCHIVED", + "supersededByRevisionId": null, + "createdAt": "" + } + ], + "pauseEras": [ + { + "id": "", + "medicationId": "", + "userId": "", + "pausedAt": "", + "resumedAt": "", + "createdAt": "" + } + ], + "courses": [ + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-08-01T00:00:00.000Z", + "endsOn": "2026-08-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": "2026-12-31T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "medicationId": "", + "userId": "", + "startsOn": "2026-10-20T00:00:00.000Z", + "endsOn": "2026-10-20T00:00:00.000Z", + "noteEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T07:00:00.000Z", + "takenAt": "2026-10-10T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": "2026-10-15T06:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T18:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T05:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "4", + "unitsRemaining": "3", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [ + { + "medicationId": "", + "category": "OTHER", + "updatedAt": "" + }, + { + "medicationId": "", + "category": "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13", + "updatedAt": "" + } + ], + "ingredients": [ + { + "id": "", + "userId": "", + "medicationId": "", + "nutrient": "vitamin_d", + "amountPerUnit": 25, + "unit": "ug", + "enteredAmount": 1000, + "enteredUnit": "IU", + "source": "MANUAL", + "createdAt": "", + "updatedAt": "" + } + ], + "audit": [ + { + "action": "medication.delete", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "name": "To delete" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Daily tablet renamed" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "active" + ], + "previous": { + "active": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": false + }, + "replacedCadence": "08:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "asNeeded" + ], + "previous": { + "asNeeded": true + }, + "replacedCadence": null, + "replacedScheduleCount": 0, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "autoMarkTaken" + ], + "previous": { + "autoMarkTaken": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "dose", + "name" + ], + "previous": { + "name": "Daily tablet", + "dose": "10mg" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + }, + "replacedCadence": "09:00 FREQ=DAILY", + "replacedScheduleCount": 1, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "endsOn" + ], + "previous": { + "endsOn": null + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "name" + ], + "previous": { + "name": "Record only" + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": false + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [ + "trackIntake" + ], + "previous": { + "trackIntake": true + } + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "08:00 FREQ=DAILY | 20:00 FREQ=DAILY", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 2, + "tombstonedFrom": "2026-10-15T18:00:00.000Z", + "tombstonedTo": "2026-10-16T06:00:00.000Z" + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {}, + "replacedCadence": "11:30 FREQ=DAILY | 20:00 i2;1,3,5", + "replacedScheduleCount": 2, + "tombstonedSlotCount": 0, + "tombstonedFrom": null, + "tombstonedTo": null + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + }, + { + "action": "medication.update", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "fields": [], + "previous": {} + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": [ + { + "name": "kit-ingest", + "permissions": [ + "medication::ingest" + ], + "revoked": true + }, + { + "name": "kit-phone", + "permissions": [ + "*" + ], + "revoked": false + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.01-mixed.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.01-mixed.json.snap new file mode 100644 index 000000000..436ec3020 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.01-mixed.json.snap @@ -0,0 +1,701 @@ +{ + "status": 200, + "body": { + "data": { + "processed": 18, + "inserted": 6, + "updated": 2, + "duplicates": 3, + "skipped": [ + { + "index": 0, + "reason": "medication_not_found" + }, + { + "index": 1, + "reason": "intake_not_tracked" + }, + { + "index": 2, + "reason": "unstable_external_id" + }, + { + "index": 5, + "reason": "force_slot_invalid" + }, + { + "index": 6, + "reason": "force_slot_occupied" + }, + { + "index": 12, + "reason": "injection_site_not_allowed" + }, + { + "index": 16, + "reason": "unstable_external_id" + } + ], + "entries": [ + { + "index": 0, + "status": "skipped", + "reason": "medication_not_found" + }, + { + "index": 1, + "status": "skipped", + "reason": "intake_not_tracked" + }, + { + "index": 2, + "status": "skipped", + "reason": "unstable_external_id" + }, + { + "index": 3, + "status": "updated", + "id": "" + }, + { + "index": 4, + "status": "inserted", + "id": "" + }, + { + "index": 5, + "status": "skipped", + "reason": "force_slot_invalid" + }, + { + "index": 6, + "status": "skipped", + "reason": "force_slot_occupied" + }, + { + "index": 7, + "status": "inserted", + "id": "" + }, + { + "index": 8, + "status": "duplicate", + "id": "" + }, + { + "index": 9, + "status": "inserted", + "id": "" + }, + { + "index": 10, + "status": "inserted", + "id": "" + }, + { + "index": 11, + "status": "duplicate", + "id": "" + }, + { + "index": 12, + "status": "skipped", + "reason": "injection_site_not_allowed" + }, + { + "index": 13, + "status": "inserted", + "id": "" + }, + { + "index": 14, + "status": "duplicate", + "id": "" + }, + { + "index": 15, + "status": "inserted", + "id": "" + }, + { + "index": 16, + "status": "skipped", + "reason": "unstable_external_id" + }, + { + "index": 17, + "status": "updated", + "id": "" + } + ] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.bulk.ingest" + }, + "meta": { + "write_bucket": "own", + "medication_id": "", + "units": 0, + "item_ids": [], + "auto_opened": 0, + "processed": 18, + "inserted": 6, + "updated": 2, + "duplicates": 3, + "skipped": 7, + "extra_doses_unscheduled": 1, + "external_id_rejected": 2, + "external_id_shapes": "object_description", + "external_id_surface": "medication.intake.bulk", + "sync_checkpoint_at": "" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "dispatchMedicationIntakeWebClearBulk", + "args": [ + { + "userId": "", + "slots": [ + { + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z" + }, + { + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z" + }, + { + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z" + }, + { + "medicationId": "", + "scheduledFor": "2026-10-16T08:00:00.000Z" + }, + { + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z" + }, + { + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z" + }, + { + "medicationId": "", + "scheduledFor": "2026-10-14T09:30:00.000Z" + } + ], + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Apple mirror", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": "APPLE_HEALTH", + "externalId": "hk-concept-0001", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "As-needed pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:00:00.000Z", + "takenAt": "2026-10-14T09:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": "hk-dose-0000", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:30:00.000Z", + "takenAt": "2026-10-14T09:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0002", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0001", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": "bulk-key-0001", + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "28", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 6, + "updated": 2, + "duplicates": 3, + "skipped": 7 + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": { + "lastSyncedAt": "", + "arrivals": [ + { + "type": "MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": "foreground", + "lastNewSampleAt": "", + "lastBackgroundAt": null, + "lastSeenDuplicateAt": null + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.02-mixed-again.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.02-mixed-again.json.snap new file mode 100644 index 000000000..3cb7d5b8a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.02-mixed-again.json.snap @@ -0,0 +1,702 @@ +{ + "status": 200, + "body": { + "data": { + "processed": 18, + "inserted": 0, + "updated": 5, + "duplicates": 6, + "skipped": [ + { + "index": 0, + "reason": "medication_not_found" + }, + { + "index": 1, + "reason": "intake_not_tracked" + }, + { + "index": 2, + "reason": "unstable_external_id" + }, + { + "index": 5, + "reason": "force_slot_invalid" + }, + { + "index": 6, + "reason": "force_slot_occupied" + }, + { + "index": 12, + "reason": "injection_site_not_allowed" + }, + { + "index": 16, + "reason": "unstable_external_id" + } + ], + "entries": [ + { + "index": 0, + "status": "skipped", + "reason": "medication_not_found" + }, + { + "index": 1, + "status": "skipped", + "reason": "intake_not_tracked" + }, + { + "index": 2, + "status": "skipped", + "reason": "unstable_external_id" + }, + { + "index": 3, + "status": "updated", + "id": "" + }, + { + "index": 4, + "status": "updated", + "id": "" + }, + { + "index": 5, + "status": "skipped", + "reason": "force_slot_invalid" + }, + { + "index": 6, + "status": "skipped", + "reason": "force_slot_occupied" + }, + { + "index": 7, + "status": "updated", + "id": "" + }, + { + "index": 8, + "status": "duplicate", + "id": "" + }, + { + "index": 9, + "status": "updated", + "id": "" + }, + { + "index": 10, + "status": "duplicate", + "id": "" + }, + { + "index": 11, + "status": "duplicate", + "id": "" + }, + { + "index": 12, + "status": "skipped", + "reason": "injection_site_not_allowed" + }, + { + "index": 13, + "status": "duplicate", + "id": "" + }, + { + "index": 14, + "status": "duplicate", + "id": "" + }, + { + "index": 15, + "status": "duplicate", + "id": "" + }, + { + "index": 16, + "status": "skipped", + "reason": "unstable_external_id" + }, + { + "index": 17, + "status": "updated", + "id": "" + } + ] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.bulk.ingest" + }, + "meta": { + "write_bucket": "own", + "processed": 18, + "inserted": 0, + "updated": 5, + "duplicates": 6, + "skipped": 7, + "extra_doses_unscheduled": 0, + "external_id_rejected": 2, + "external_id_shapes": "object_description", + "external_id_surface": "medication.intake.bulk", + "sync_checkpoint_at": "" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "dispatchMedicationIntakeWebClearBulk", + "args": [ + { + "userId": "", + "slots": [ + { + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z" + }, + { + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z" + }, + { + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z" + }, + { + "medicationId": "", + "scheduledFor": "2026-10-16T08:00:00.000Z" + }, + { + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z" + } + ], + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Apple mirror", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": "APPLE_HEALTH", + "externalId": "hk-concept-0001", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "As-needed pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:00:00.000Z", + "takenAt": "2026-10-14T09:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": "hk-dose-0000", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:30:00.000Z", + "takenAt": "2026-10-14T09:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0002", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0001", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": "bulk-key-0001", + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "28", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 0, + "updated": 5, + "duplicates": 6, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 6, + "updated": 2, + "duplicates": 3, + "skipped": 7 + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": { + "lastSyncedAt": "", + "arrivals": [ + { + "type": "MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": "foreground", + "lastNewSampleAt": "", + "lastBackgroundAt": null, + "lastSeenDuplicateAt": "" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03a-background.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03a-background.json.snap new file mode 100644 index 000000000..eebf4724a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03a-background.json.snap @@ -0,0 +1,607 @@ +{ + "status": 200, + "body": { + "data": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": [], + "entries": [ + { + "index": 0, + "status": "inserted", + "id": "" + } + ] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.bulk.ingest" + }, + "meta": { + "write_bucket": "own", + "medication_id": "", + "units": 0, + "item_ids": [], + "auto_opened": 0, + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0, + "extra_doses_unscheduled": 0, + "sync_checkpoint_at": "" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "dispatchMedicationIntakeWebClearBulk", + "args": [ + { + "userId": "", + "slots": [ + { + "medicationId": "", + "scheduledFor": "2026-10-13T09:00:00.000Z" + } + ], + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Apple mirror", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": "APPLE_HEALTH", + "externalId": "hk-concept-0001", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "As-needed pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T09:00:00.000Z", + "takenAt": "2026-10-13T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0003", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:00:00.000Z", + "takenAt": "2026-10-14T09:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": "hk-dose-0000", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:30:00.000Z", + "takenAt": "2026-10-14T09:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0002", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0001", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": "bulk-key-0001", + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "28", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 0, + "updated": 5, + "duplicates": 6, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 6, + "updated": 2, + "duplicates": 3, + "skipped": 7 + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": { + "lastSyncedAt": "", + "arrivals": [ + { + "type": "MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": "background", + "lastNewSampleAt": "", + "lastBackgroundAt": "", + "lastSeenDuplicateAt": "" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03b-backfill.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03b-backfill.json.snap new file mode 100644 index 000000000..628b2b978 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.03b-backfill.json.snap @@ -0,0 +1,648 @@ +{ + "status": 200, + "body": { + "data": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": [], + "entries": [ + { + "index": 0, + "status": "inserted", + "id": "" + } + ] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.bulk.ingest" + }, + "meta": { + "write_bucket": "own", + "medication_id": "", + "units": 0, + "item_ids": [], + "auto_opened": 0, + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0, + "extra_doses_unscheduled": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "dispatchMedicationIntakeWebClearBulk", + "args": [ + { + "userId": "", + "slots": [ + { + "medicationId": "", + "scheduledFor": "2026-10-12T09:00:00.000Z" + } + ], + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Apple mirror", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": "APPLE_HEALTH", + "externalId": "hk-concept-0001", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "As-needed pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T09:00:00.000Z", + "takenAt": "2026-10-12T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0004", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T09:00:00.000Z", + "takenAt": "2026-10-13T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0003", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:00:00.000Z", + "takenAt": "2026-10-14T09:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": "hk-dose-0000", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:30:00.000Z", + "takenAt": "2026-10-14T09:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0002", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0001", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": "bulk-key-0001", + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "28", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 0, + "updated": 5, + "duplicates": 6, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 6, + "updated": 2, + "duplicates": 3, + "skipped": 7 + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": { + "lastSyncedAt": "", + "arrivals": [ + { + "type": "MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": "background", + "lastNewSampleAt": "", + "lastBackgroundAt": "", + "lastSeenDuplicateAt": "" + }, + { + "type": "backfill:MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": null, + "lastNewSampleAt": null, + "lastBackgroundAt": null, + "lastSeenDuplicateAt": null + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04a-too-large.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04a-too-large.json.snap new file mode 100644 index 000000000..3b6ab3db5 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04a-too-large.json.snap @@ -0,0 +1,595 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Batch exceeds the 500-entry limit", + "meta": { + "errorCode": "medications.intake.bulk.too_large", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": { + "write_bucket": "own" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Apple mirror", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": "APPLE_HEALTH", + "externalId": "hk-concept-0001", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "As-needed pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T09:00:00.000Z", + "takenAt": "2026-10-12T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0004", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T09:00:00.000Z", + "takenAt": "2026-10-13T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0003", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:00:00.000Z", + "takenAt": "2026-10-14T09:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": "hk-dose-0000", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:30:00.000Z", + "takenAt": "2026-10-14T09:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0002", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0001", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": "bulk-key-0001", + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "28", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 0, + "updated": 5, + "duplicates": 6, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 6, + "updated": 2, + "duplicates": 3, + "skipped": 7 + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": { + "lastSyncedAt": "", + "arrivals": [ + { + "type": "MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": "background", + "lastNewSampleAt": "", + "lastBackgroundAt": "", + "lastSeenDuplicateAt": "" + }, + { + "type": "backfill:MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": null, + "lastNewSampleAt": null, + "lastBackgroundAt": null, + "lastSeenDuplicateAt": null + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04b-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04b-invalid.json.snap new file mode 100644 index 000000000..4d2dad010 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04b-invalid.json.snap @@ -0,0 +1,630 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Validation failed", + "details": { + "issues": [ + { + "path": "entries.0.medicationId", + "code": "too_small", + "message": "Too small: expected string to have >=1 characters" + }, + { + "path": "entries.0.takenAt", + "code": "invalid_format", + "message": "Invalid ISO datetime" + } + ] + }, + "meta": { + "errorCode": "medications.intake.bulk.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "medications.intake.bulk.validation-failed" + }, + "meta": { + "write_bucket": "own", + "issue_count": 2 + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Apple mirror", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": "APPLE_HEALTH", + "externalId": "hk-concept-0001", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "As-needed pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T09:00:00.000Z", + "takenAt": "2026-10-12T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0004", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T09:00:00.000Z", + "takenAt": "2026-10-13T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0003", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:00:00.000Z", + "takenAt": "2026-10-14T09:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": "hk-dose-0000", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:30:00.000Z", + "takenAt": "2026-10-14T09:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0002", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0001", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": "bulk-key-0001", + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "28", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 0, + "updated": 5, + "duplicates": 6, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 6, + "updated": 2, + "duplicates": 3, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.validation-failed", + "userId": "", + "actorUserId": null, + "details": { + "issues": [ + { + "path": "entries.0.medicationId", + "code": "too_small" + }, + { + "path": "entries.0.takenAt", + "code": "invalid_format" + } + ] + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": { + "lastSyncedAt": "", + "arrivals": [ + { + "type": "MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": "background", + "lastNewSampleAt": "", + "lastBackgroundAt": "", + "lastSeenDuplicateAt": "" + }, + { + "type": "backfill:MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": null, + "lastNewSampleAt": null, + "lastBackgroundAt": null, + "lastSeenDuplicateAt": null + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04c-apple-not-mirrored.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04c-apple-not-mirrored.json.snap new file mode 100644 index 000000000..4249b41f7 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.04c-apple-not-mirrored.json.snap @@ -0,0 +1,613 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "An APPLE_HEALTH entry may only target a medication mirrored from Apple Health", + "meta": { + "errorCode": "medications.intake.bulk.apple_health_not_mirrored", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": { + "write_bucket": "own" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Apple mirror", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": "APPLE_HEALTH", + "externalId": "hk-concept-0001", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "As-needed pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T09:00:00.000Z", + "takenAt": "2026-10-12T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0004", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T09:00:00.000Z", + "takenAt": "2026-10-13T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0003", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:00:00.000Z", + "takenAt": "2026-10-14T09:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": "hk-dose-0000", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:30:00.000Z", + "takenAt": "2026-10-14T09:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0002", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0001", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": "bulk-key-0001", + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "28", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 0, + "updated": 5, + "duplicates": 6, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 6, + "updated": 2, + "duplicates": 3, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.validation-failed", + "userId": "", + "actorUserId": null, + "details": { + "issues": [ + { + "path": "entries.0.medicationId", + "code": "too_small" + }, + { + "path": "entries.0.takenAt", + "code": "invalid_format" + } + ] + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": { + "lastSyncedAt": "", + "arrivals": [ + { + "type": "MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": "background", + "lastNewSampleAt": "", + "lastBackgroundAt": "", + "lastSeenDuplicateAt": "" + }, + { + "type": "backfill:MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": null, + "lastNewSampleAt": null, + "lastBackgroundAt": null, + "lastSeenDuplicateAt": null + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05a-native.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05a-native.json.snap new file mode 100644 index 000000000..6656a318d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05a-native.json.snap @@ -0,0 +1,708 @@ +{ + "status": 200, + "body": { + "data": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": [], + "entries": [ + { + "index": 0, + "status": "inserted", + "id": "" + } + ] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.bulk.ingest" + }, + "meta": { + "write_bucket": "own", + "medication_id": "", + "units": 1, + "item_ids": [ + "" + ], + "auto_opened": 0, + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0, + "extra_doses_unscheduled": 0, + "sync_checkpoint_at": "" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "dispatchMedicationIntakeWebClearBulk", + "args": [ + { + "userId": "", + "slots": [ + { + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z" + } + ], + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Apple mirror", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": "APPLE_HEALTH", + "externalId": "hk-concept-0001", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "As-needed pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T09:00:00.000Z", + "takenAt": "2026-10-12T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0004", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T09:00:00.000Z", + "takenAt": "2026-10-13T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0003", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:00:00.000Z", + "takenAt": "2026-10-14T09:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": "hk-dose-0000", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:30:00.000Z", + "takenAt": "2026-10-14T09:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0002", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0001", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": "bulk-key-0001", + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 0, + "updated": 5, + "duplicates": 6, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 6, + "updated": 2, + "duplicates": 3, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.validation-failed", + "userId": "", + "actorUserId": null, + "details": { + "issues": [ + { + "path": "entries.0.medicationId", + "code": "too_small" + }, + { + "path": "entries.0.takenAt", + "code": "invalid_format" + } + ] + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": { + "lastSyncedAt": "", + "arrivals": [ + { + "type": "MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": "background", + "lastNewSampleAt": "", + "lastBackgroundAt": "", + "lastSeenDuplicateAt": "" + }, + { + "type": "backfill:MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": null, + "lastNewSampleAt": null, + "lastBackgroundAt": null, + "lastSeenDuplicateAt": null + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05b-narrow-token.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05b-narrow-token.json.snap new file mode 100644 index 000000000..216e2e279 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05b-narrow-token.json.snap @@ -0,0 +1,663 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "Insufficient permissions", + "meta": { + "errorCode": "auth.scope.insufficient", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Insufficient permissions" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Apple mirror", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": "APPLE_HEALTH", + "externalId": "hk-concept-0001", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "As-needed pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T09:00:00.000Z", + "takenAt": "2026-10-12T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0004", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T09:00:00.000Z", + "takenAt": "2026-10-13T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0003", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:00:00.000Z", + "takenAt": "2026-10-14T09:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": "hk-dose-0000", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:30:00.000Z", + "takenAt": "2026-10-14T09:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0002", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0001", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": "bulk-key-0001", + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "auth.bearer.failure", + "userId": "", + "actorUserId": null, + "details": { + "reason": "undeclared_scope", + "tokenId": "" + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 0, + "updated": 5, + "duplicates": 6, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 6, + "updated": 2, + "duplicates": 3, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.validation-failed", + "userId": "", + "actorUserId": null, + "details": { + "issues": [ + { + "path": "entries.0.medicationId", + "code": "too_small" + }, + { + "path": "entries.0.takenAt", + "code": "invalid_format" + } + ] + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": { + "lastSyncedAt": "", + "arrivals": [ + { + "type": "MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": "background", + "lastNewSampleAt": "", + "lastBackgroundAt": "", + "lastSeenDuplicateAt": "" + }, + { + "type": "backfill:MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": null, + "lastNewSampleAt": null, + "lastBackgroundAt": null, + "lastSeenDuplicateAt": null + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05c-delegate.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05c-delegate.json.snap new file mode 100644 index 000000000..ef83ea41b --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05c-delegate.json.snap @@ -0,0 +1,665 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "This endpoint cannot be used while acting on another account", + "meta": { + "errorCode": "sharing.not_permitted", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": null, + "meta": { + "sharing_refusal": "undeclared_mode" + }, + "warnings": null, + "error": { + "type": "SharingNotPermittedError", + "message": "This endpoint cannot be used while acting on another account" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Apple mirror", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": "APPLE_HEALTH", + "externalId": "hk-concept-0001", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "As-needed pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T09:00:00.000Z", + "takenAt": "2026-10-12T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0004", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T09:00:00.000Z", + "takenAt": "2026-10-13T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0003", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:00:00.000Z", + "takenAt": "2026-10-14T09:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": "hk-dose-0000", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:30:00.000Z", + "takenAt": "2026-10-14T09:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0002", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0001", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": "bulk-key-0001", + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "auth.bearer.failure", + "userId": "", + "actorUserId": null, + "details": { + "reason": "undeclared_scope", + "tokenId": "" + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 0, + "updated": 5, + "duplicates": 6, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 6, + "updated": 2, + "duplicates": 3, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.validation-failed", + "userId": "", + "actorUserId": null, + "details": { + "issues": [ + { + "path": "entries.0.medicationId", + "code": "too_small" + }, + { + "path": "entries.0.takenAt", + "code": "invalid_format" + } + ] + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": { + "lastSyncedAt": "", + "arrivals": [ + { + "type": "MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": "background", + "lastNewSampleAt": "", + "lastBackgroundAt": "", + "lastSeenDuplicateAt": "" + }, + { + "type": "backfill:MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": null, + "lastNewSampleAt": null, + "lastBackgroundAt": null, + "lastSeenDuplicateAt": null + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05d-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05d-anonymous.json.snap new file mode 100644 index 000000000..8fff49289 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-bulk.05d-anonymous.json.snap @@ -0,0 +1,663 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Apple mirror", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": "APPLE_HEALTH", + "externalId": "hk-concept-0001", + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "As-needed pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T09:00:00.000Z", + "takenAt": "2026-10-12T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0004", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T09:00:00.000Z", + "takenAt": "2026-10-13T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0003", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:00:00.000Z", + "takenAt": "2026-10-14T09:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": "hk-dose-0000", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T09:30:00.000Z", + "takenAt": "2026-10-14T09:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0002", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "APPLE_HEALTH", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": "hk-dose-0001", + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": "bulk-key-0001", + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-16T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "auth.bearer.failure", + "userId": "", + "actorUserId": null, + "details": { + "reason": "undeclared_scope", + "tokenId": "" + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 1, + "inserted": 1, + "updated": 0, + "duplicates": 0, + "skipped": 0 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 0, + "updated": 5, + "duplicates": 6, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.ingest", + "userId": "", + "actorUserId": null, + "details": { + "processed": 18, + "inserted": 6, + "updated": 2, + "duplicates": 3, + "skipped": 7 + }, + "ipAddress": null + }, + { + "action": "medications.intake.bulk.validation-failed", + "userId": "", + "actorUserId": null, + "details": { + "issues": [ + { + "path": "entries.0.medicationId", + "code": "too_small" + }, + { + "path": "entries.0.takenAt", + "code": "invalid_format" + } + ] + }, + "ipAddress": null + } + ], + "idempotency": [] + }, + "extra": { + "lastSyncedAt": "", + "arrivals": [ + { + "type": "MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": "background", + "lastNewSampleAt": "", + "lastBackgroundAt": "", + "lastSeenDuplicateAt": "" + }, + { + "type": "backfill:MEDICATION_DOSE", + "lastReceivedAt": "", + "lastTrigger": null, + "lastNewSampleAt": null, + "lastBackgroundAt": null, + "lastSeenDuplicateAt": null + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.01-today.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.01-today.json.snap new file mode 100644 index 000000000..542dd90b5 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.01-today.json.snap @@ -0,0 +1,685 @@ +{ + "status": 200, + "body": { + "data": [ + { + "id": "", + "medicationId": "", + "scheduledAt": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "status": "missed", + "snoozedUntil": null + }, + { + "id": "", + "medicationId": "", + "scheduledAt": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "status": "snoozed", + "snoozedUntil": "2026-10-15T12:30:00.000Z" + }, + { + "id": "", + "medicationId": "", + "scheduledAt": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "status": "pending", + "snoozedUntil": null + }, + { + "id": "", + "medicationId": "", + "scheduledAt": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "status": "taken", + "snoozedUntil": null + }, + { + "id": "", + "medicationId": "", + "scheduledAt": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "status": "skipped", + "snoozedUntil": null + }, + { + "id": "", + "medicationId": "", + "scheduledAt": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "status": "snoozed", + "snoozedUntil": "2026-10-15T12:30:00.000Z" + } + ], + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.today" + }, + "meta": { + "count": 6, + "projected": 6, + "backfilled": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "30", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.02a-compliance-cold.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.02a-compliance-cold.json.snap new file mode 100644 index 000000000..6296a686c --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.02a-compliance-cold.json.snap @@ -0,0 +1,673 @@ +{ + "status": 200, + "body": { + "data": [ + { + "date": "2026-10-09", + "scheduled": 6, + "taken": 0 + }, + { + "date": "2026-10-10", + "scheduled": 6, + "taken": 1 + }, + { + "date": "2026-10-11", + "scheduled": 6, + "taken": 0 + }, + { + "date": "2026-10-12", + "scheduled": 6, + "taken": 1 + }, + { + "date": "2026-10-13", + "scheduled": 6, + "taken": 0 + }, + { + "date": "2026-10-14", + "scheduled": 6, + "taken": 0 + }, + { + "date": "2026-10-15", + "scheduled": 5, + "taken": 1 + } + ], + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.compliance" + }, + "meta": { + "cache.medicationsIntake.outcome": "miss", + "cache.medicationsIntake.key_hash": "", + "days": 7, + "count": 7 + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "30", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.02b-compliance-warm.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.02b-compliance-warm.json.snap new file mode 100644 index 000000000..20f1f535b --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.02b-compliance-warm.json.snap @@ -0,0 +1,673 @@ +{ + "status": 200, + "body": { + "data": [ + { + "date": "2026-10-09", + "scheduled": 6, + "taken": 0 + }, + { + "date": "2026-10-10", + "scheduled": 6, + "taken": 1 + }, + { + "date": "2026-10-11", + "scheduled": 6, + "taken": 0 + }, + { + "date": "2026-10-12", + "scheduled": 6, + "taken": 1 + }, + { + "date": "2026-10-13", + "scheduled": 6, + "taken": 0 + }, + { + "date": "2026-10-14", + "scheduled": 6, + "taken": 0 + }, + { + "date": "2026-10-15", + "scheduled": 5, + "taken": 1 + } + ], + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.compliance" + }, + "meta": { + "cache.medicationsIntake.outcome": "hit", + "cache.medicationsIntake.key_hash": "", + "days": 7, + "count": 7 + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "30", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.03-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.03-invalid.json.snap new file mode 100644 index 000000000..9de2f573c --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.03-invalid.json.snap @@ -0,0 +1,653 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Validation failed", + "details": { + "issues": [ + { + "path": "scope", + "code": "invalid_value", + "message": "Invalid option: expected one of \"today\"|\"compliance\"" + }, + { + "path": "days", + "code": "too_small", + "message": "Too small: expected number to be >=1" + } + ] + }, + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "medications.intake.list.validation-failed" + }, + "meta": { + "issue_count": 2, + "rejected_attempt": "medications.intake.list.validation-failed" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "30", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.04a-reader.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.04a-reader.json.snap new file mode 100644 index 000000000..542dd90b5 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.04a-reader.json.snap @@ -0,0 +1,685 @@ +{ + "status": 200, + "body": { + "data": [ + { + "id": "", + "medicationId": "", + "scheduledAt": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "status": "missed", + "snoozedUntil": null + }, + { + "id": "", + "medicationId": "", + "scheduledAt": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "status": "snoozed", + "snoozedUntil": "2026-10-15T12:30:00.000Z" + }, + { + "id": "", + "medicationId": "", + "scheduledAt": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "status": "pending", + "snoozedUntil": null + }, + { + "id": "", + "medicationId": "", + "scheduledAt": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "status": "taken", + "snoozedUntil": null + }, + { + "id": "", + "medicationId": "", + "scheduledAt": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "status": "skipped", + "snoozedUntil": null + }, + { + "id": "", + "medicationId": "", + "scheduledAt": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "status": "snoozed", + "snoozedUntil": "2026-10-15T12:30:00.000Z" + } + ], + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.today" + }, + "meta": { + "count": 6, + "projected": 6, + "backfilled": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "30", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.04b-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.04b-anonymous.json.snap new file mode 100644 index 000000000..3016b5ab1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.04b-anonymous.json.snap @@ -0,0 +1,637 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "ACTIVE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "30", + "firstUseAt": null, + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.11-take-in-place.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.11-take-in-place.json.snap new file mode 100644 index 000000000..6001b05ef --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.11-take-in-place.json.snap @@ -0,0 +1,716 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.update" + }, + "meta": { + "medication_id": "", + "units": 1, + "item_ids": [ + "" + ], + "auto_opened": 1, + "intakeId": "", + "status": "taken" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "29", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.12-take-slot-move.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.12-take-slot-move.json.snap new file mode 100644 index 000000000..d6d98517a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.12-take-slot-move.json.snap @@ -0,0 +1,740 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "idempotencyKey": null, + "scheduledFor": "2026-10-14T20:00:00.000Z", + "source": "WEB", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.update" + }, + "meta": { + "medication_id": "", + "units": 1, + "item_ids": [ + "" + ], + "auto_opened": 0, + "intakeId": "", + "status": "taken" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "28", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.13a-pin.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.13a-pin.json.snap new file mode 100644 index 000000000..666d31663 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.13a-pin.json.snap @@ -0,0 +1,776 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "idempotencyKey": null, + "scheduledFor": "2026-10-13T08:00:00.000Z", + "source": "WEB", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.update" + }, + "meta": { + "medication_id": "", + "units": 1, + "item_ids": [ + "" + ], + "auto_opened": 0, + "intakeId": "", + "status": "taken" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.13b-pin-not-a-slot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.13b-pin-not-a-slot.json.snap new file mode 100644 index 000000000..11df637ed --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.13b-pin-not-a-slot.json.snap @@ -0,0 +1,727 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "forceSlotInstant is not a scheduled slot of this medication", + "meta": { + "errorCode": "medications.intake.force_slot.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "medication.intake.force_slot.invalid" + }, + "meta": { + "medication_id": "", + "intake_id": "" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.14-skip.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.14-skip.json.snap new file mode 100644 index 000000000..522843f52 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.14-skip.json.snap @@ -0,0 +1,792 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.update" + }, + "meta": { + "intakeId": "", + "status": "skipped" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "skipped" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.15a-snooze-until.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.15a-snooze-until.json.snap new file mode 100644 index 000000000..80c9de305 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.15a-snooze-until.json.snap @@ -0,0 +1,784 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.update" + }, + "meta": { + "intakeId": "", + "status": "snoozed" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "snoozed" + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T13:00:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.15b-snooze-default.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.15b-snooze-default.json.snap new file mode 100644 index 000000000..7ab938329 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.15b-snooze-default.json.snap @@ -0,0 +1,794 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.update" + }, + "meta": { + "intakeId": "", + "status": "snoozed" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "snoozed" + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.16a-unknown.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.16a-unknown.json.snap new file mode 100644 index 000000000..fba272828 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.16a-unknown.json.snap @@ -0,0 +1,752 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Intake event not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.16b-foreign.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.16b-foreign.json.snap new file mode 100644 index 000000000..fba272828 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.16b-foreign.json.snap @@ -0,0 +1,752 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Intake event not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.16c-tombstoned.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.16c-tombstoned.json.snap new file mode 100644 index 000000000..fba272828 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.16c-tombstoned.json.snap @@ -0,0 +1,752 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Intake event not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.17a-record-only-open.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.17a-record-only-open.json.snap new file mode 100644 index 000000000..547106503 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.17a-record-only-open.json.snap @@ -0,0 +1,752 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "This medication is kept as a record; its intake is not tracked", + "meta": { + "errorCode": "medication.intake.notTracked", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.17b-record-only-correction.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.17b-record-only-correction.json.snap new file mode 100644 index 000000000..e314edc22 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.17b-record-only-correction.json.snap @@ -0,0 +1,822 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.update" + }, + "meta": { + "intakeId": "", + "status": "taken" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.18a-site-not-allowed.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.18a-site-not-allowed.json.snap new file mode 100644 index 000000000..df23d680d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.18a-site-not-allowed.json.snap @@ -0,0 +1,767 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Injection site is not allowed for this medication", + "meta": { + "errorCode": "medications.intake.injection_site.disallowed", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "medication.intake.injection_site.disallowed" + }, + "meta": { + "medication_id": "", + "site": "ABDOMEN_RIGHT" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.18b-site.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.18b-site.json.snap new file mode 100644 index 000000000..0579f7e56 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.18b-site.json.snap @@ -0,0 +1,836 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.update" + }, + "meta": { + "medication_id": "", + "units": 0, + "item_ids": [], + "auto_opened": 0, + "intakeId": "", + "status": "taken" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.19-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.19-invalid.json.snap new file mode 100644 index 000000000..d124ccc0f --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.19-invalid.json.snap @@ -0,0 +1,791 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Validation failed", + "details": { + "issues": [ + { + "path": "intakeId", + "code": "too_small", + "message": "Too small: expected string to have >=1 characters" + }, + { + "path": "status", + "code": "invalid_value", + "message": "Invalid option: expected one of \"taken\"|\"skipped\"|\"snoozed\"" + } + ] + }, + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "medications.intake.update.validation-failed" + }, + "meta": { + "issue_count": 2, + "rejected_attempt": "medications.intake.update.validation-failed" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": "2026-10-15T12:30:00.000Z", + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20a-writer-take.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20a-writer-take.json.snap new file mode 100644 index 000000000..bd0c5ac92 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20a-writer-take.json.snap @@ -0,0 +1,830 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "takenAt": "", + "skipped": false, + "idempotencyKey": null, + "scheduledFor": "2026-10-15T08:00:00.000Z", + "source": "REMINDER", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medications.intake.update" + }, + "meta": { + "intakeId": "", + "status": "taken" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": "", + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20b-writer-snooze-refused.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20b-writer-snooze-refused.json.snap new file mode 100644 index 000000000..8dd33d85b --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20b-writer-snooze-refused.json.snap @@ -0,0 +1,797 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "Snoozing a reminder is not part of shared access", + "meta": { + "errorCode": "sharing.not_permitted", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": { + "name": "medications.intake.update.refused" + }, + "meta": { + "reason": "snooze" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": "", + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "intakeId": "", + "status": "snoozed", + "reason": "snooze" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20c-writer-correction-refused.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20c-writer-correction-refused.json.snap new file mode 100644 index 000000000..4083a8d7e --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20c-writer-correction-refused.json.snap @@ -0,0 +1,808 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "Changing a dose that is already recorded is not part of shared access", + "meta": { + "errorCode": "sharing.not_permitted", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": { + "name": "medications.intake.update.refused" + }, + "meta": { + "reason": "resolved_event" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": "", + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "intakeId": "", + "status": "skipped", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "intakeId": "", + "status": "snoozed", + "reason": "snooze" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20d-reader.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20d-reader.json.snap new file mode 100644 index 000000000..2f40671a2 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20d-reader.json.snap @@ -0,0 +1,809 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "Account access denied", + "meta": { + "errorCode": "sharing.access.denied", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": null, + "meta": { + "sharing_refusal": "insufficient_access" + }, + "warnings": null, + "error": { + "type": "SharingAccessDeniedError", + "message": "Account access denied" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": "", + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "intakeId": "", + "status": "skipped", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "intakeId": "", + "status": "snoozed", + "reason": "snooze" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake-status.20e-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20e-anonymous.json.snap new file mode 100644 index 000000000..76f803a7e --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake-status.20e-anonymous.json.snap @@ -0,0 +1,807 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "Daily pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Three times", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "06:00", + "windowEnd": "07:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "06:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "10:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "10:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "13:00", + "windowEnd": "14:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "13:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-09T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "2026-10-09T09:00:00.000Z" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-10T08:00:00.000Z", + "takenAt": "2026-10-10T08:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T07:00:00.000Z", + "takenAt": "2026-10-13T07:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-13T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T07:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T06:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": true, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T10:00:00.000Z", + "takenAt": "2026-10-15T10:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T13:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": "" + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": "", + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "skipped" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "snoozed" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update", + "userId": "", + "actorUserId": null, + "details": { + "intakeId": "", + "status": "taken" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "intakeId": "", + "status": "skipped", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "intakeId": "", + "status": "snoozed", + "reason": "snooze" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.01-take-in-band.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.01-take-in-band.json.snap new file mode 100644 index 000000000..b77e18c79 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.01-take-in-band.json.snap @@ -0,0 +1,488 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "idempotencyKey": null, + "scheduledFor": "2026-10-15T08:00:00.000Z", + "source": "REMINDER", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.intake", + "entity_type": "intake_event", + "entity_id": "" + }, + "meta": { + "medication_id": "", + "units": 1, + "item_ids": [ + "" + ], + "auto_opened": 1, + "skipped": false + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "29", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.02a-take-standalone.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.02a-take-standalone.json.snap new file mode 100644 index 000000000..5854deab4 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.02a-take-standalone.json.snap @@ -0,0 +1,530 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": "2026-10-15T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.intake", + "entity_type": "intake_event", + "entity_id": "" + }, + "meta": { + "medication_id": "", + "units": 0, + "item_ids": [], + "auto_opened": 0, + "skipped": false + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": "2026-10-15T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "29", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.02b-take-standalone-repeat.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.02b-take-standalone-repeat.json.snap new file mode 100644 index 000000000..193515ce6 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.02b-take-standalone-repeat.json.snap @@ -0,0 +1,481 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": "2026-10-15T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": "2026-10-15T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "29", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.03a-skip-named-slot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.03a-skip-named-slot.json.snap new file mode 100644 index 000000000..ad845eda4 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.03a-skip-named-slot.json.snap @@ -0,0 +1,547 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": null, + "skipped": true, + "idempotencyKey": null, + "scheduledFor": "2026-10-15T20:00:00.000Z", + "source": "WEB", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.intake", + "entity_type": "intake_event", + "entity_id": "" + }, + "meta": { + "medication_id": "", + "skipped": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "skipped" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": "2026-10-15T08:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": "2026-10-15T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "29", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.03b-skip-slotless.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.03b-skip-slotless.json.snap new file mode 100644 index 000000000..e7d80ad2d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.03b-skip-slotless.json.snap @@ -0,0 +1,557 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": null, + "skipped": true, + "idempotencyKey": null, + "scheduledFor": "2026-10-15T08:00:00.000Z", + "source": "REMINDER", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.intake", + "entity_type": "intake_event", + "entity_id": "" + }, + "meta": { + "medication_id": "", + "units": 1, + "item_ids": [ + "" + ], + "skipped": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "skipped" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": "2026-10-15T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "30", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.03c-delegate-skip-standalone-refused.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.03c-delegate-skip-standalone-refused.json.snap new file mode 100644 index 000000000..ed16d5992 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.03c-delegate-skip-standalone-refused.json.snap @@ -0,0 +1,517 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "Changing a dose that is already recorded is not part of shared access", + "meta": { + "errorCode": "sharing.not_permitted", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": { + "name": "medications.intake.update.refused" + }, + "meta": { + "reason": "resolved_event" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": "2026-10-15T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "30", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.03d-owner-skip-standalone.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.03d-owner-skip-standalone.json.snap new file mode 100644 index 000000000..ea57265ca --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.03d-owner-skip-standalone.json.snap @@ -0,0 +1,574 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": null, + "skipped": true, + "idempotencyKey": null, + "scheduledFor": "2026-10-15T11:00:00.000Z", + "source": "WEB", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.intake", + "entity_type": "intake_event", + "entity_id": "" + }, + "meta": { + "medication_id": "", + "skipped": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "skipped" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "30", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.04a-pin.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.04a-pin.json.snap new file mode 100644 index 000000000..b2b4705c1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.04a-pin.json.snap @@ -0,0 +1,616 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "idempotencyKey": null, + "scheduledFor": "2026-10-14T08:00:00.000Z", + "source": "WEB", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.intake", + "entity_type": "intake_event", + "entity_id": "" + }, + "meta": { + "medication_id": "", + "units": 1, + "item_ids": [ + "" + ], + "auto_opened": 0, + "skipped": false + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "29", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.04b-pin-not-a-slot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.04b-pin-not-a-slot.json.snap new file mode 100644 index 000000000..b9c5ed52a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.04b-pin-not-a-slot.json.snap @@ -0,0 +1,565 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "forceSlotInstant is not a scheduled slot of this medication", + "meta": { + "errorCode": "medications.intake.force_slot.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "medication.intake.force_slot.invalid" + }, + "meta": { + "medication_id": "" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "29", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.04c-pin-occupied.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.04c-pin-occupied.json.snap new file mode 100644 index 000000000..58bfa457d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.04c-pin-occupied.json.snap @@ -0,0 +1,565 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "forceSlotInstant already carries a recorded dose action", + "meta": { + "errorCode": "medications.intake.force_slot.occupied", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "medication.intake.force_slot.occupied" + }, + "meta": { + "medication_id": "" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": null, + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "29", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.05-named-backfill.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.05-named-backfill.json.snap new file mode 100644 index 000000000..ea65b8d16 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.05-named-backfill.json.snap @@ -0,0 +1,632 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "idempotencyKey": null, + "scheduledFor": "2026-10-13T08:00:00.000Z", + "source": "REMINDER", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.intake", + "entity_type": "intake_event", + "entity_id": "" + }, + "meta": { + "medication_id": "", + "units": 1, + "item_ids": [ + "" + ], + "auto_opened": 0, + "skipped": false + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "28", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.06a-body-key.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.06a-body-key.json.snap new file mode 100644 index 000000000..a5b9e6892 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.06a-body-key.json.snap @@ -0,0 +1,669 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "idempotencyKey": "dose-key-0001", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "source": "WEB", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.intake", + "entity_type": "intake_event", + "entity_id": "" + }, + "meta": { + "medication_id": "", + "units": 1, + "item_ids": [ + "" + ], + "auto_opened": 0, + "skipped": false + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.06b-body-key-replay.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.06b-body-key-replay.json.snap new file mode 100644 index 000000000..3f0b28077 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.06b-body-key-replay.json.snap @@ -0,0 +1,635 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "27", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.07a-header-key.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.07a-header-key.json.snap new file mode 100644 index 000000000..f4e433b3e --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.07a-header-key.json.snap @@ -0,0 +1,715 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "idempotencyKey": null, + "scheduledFor": "2026-10-12T08:00:00.000Z", + "source": "WEB", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.intake", + "entity_type": "intake_event", + "entity_id": "" + }, + "meta": { + "medication_id": "", + "units": 1, + "item_ids": [ + "" + ], + "auto_opened": 0, + "skipped": false + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "26", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.07b-header-key-replay.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.07b-header-key-replay.json.snap new file mode 100644 index 000000000..e66eeed11 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.07b-header-key-replay.json.snap @@ -0,0 +1,669 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "idempotencyKey": null, + "scheduledFor": "2026-10-12T08:00:00.000Z", + "source": "WEB", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "idempotency.replay" + }, + "meta": { + "method": "POST", + "path": "/api/medications//intake" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "26", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.08-native.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.08-native.json.snap new file mode 100644 index 000000000..fdc949900 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.08-native.json.snap @@ -0,0 +1,752 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "idempotencyKey": null, + "scheduledFor": "2026-10-11T20:00:00.000Z", + "source": "API", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.intake", + "entity_type": "intake_event", + "entity_id": "" + }, + "meta": { + "medication_id": "", + "units": 1, + "item_ids": [ + "" + ], + "auto_opened": 0, + "skipped": false + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "25", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.09a-delegate-skip-refused.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.09a-delegate-skip-refused.json.snap new file mode 100644 index 000000000..4943b7232 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.09a-delegate-skip-refused.json.snap @@ -0,0 +1,711 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "Changing a dose that is already recorded is not part of shared access", + "meta": { + "errorCode": "sharing.not_permitted", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": { + "name": "medications.intake.update.refused" + }, + "meta": { + "reason": "resolved_event" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": "2026-10-14T20:05:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "25", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.09b-owner-skip-flip.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.09b-owner-skip-flip.json.snap new file mode 100644 index 000000000..3b22d87c1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.09b-owner-skip-flip.json.snap @@ -0,0 +1,768 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": null, + "skipped": true, + "idempotencyKey": null, + "scheduledFor": "2026-10-14T20:00:00.000Z", + "source": "WEB", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.intake", + "entity_type": "intake_event", + "entity_id": "" + }, + "meta": { + "medication_id": "", + "skipped": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "skipped" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "25", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.10a-writer-take.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.10a-writer-take.json.snap new file mode 100644 index 000000000..30e91414f --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.10a-writer-take.json.snap @@ -0,0 +1,810 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "idempotencyKey": null, + "scheduledFor": "2026-10-11T08:00:00.000Z", + "source": "WEB", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.intake", + "entity_type": "intake_event", + "entity_id": "" + }, + "meta": { + "medication_id": "", + "units": 1, + "item_ids": [ + "" + ], + "auto_opened": 0, + "skipped": false + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.10b-reader.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.10b-reader.json.snap new file mode 100644 index 000000000..ed6df220c --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.10b-reader.json.snap @@ -0,0 +1,760 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "Account access denied", + "meta": { + "errorCode": "sharing.access.denied", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": null, + "meta": { + "sharing_refusal": "insufficient_access" + }, + "warnings": null, + "error": { + "type": "SharingAccessDeniedError", + "message": "Account access denied" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.10c-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.10c-anonymous.json.snap new file mode 100644 index 000000000..249b68b7d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.10c-anonymous.json.snap @@ -0,0 +1,758 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.11a-site-not-allowed.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.11a-site-not-allowed.json.snap new file mode 100644 index 000000000..717ee34c5 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.11a-site-not-allowed.json.snap @@ -0,0 +1,760 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Injection site is not allowed for this medication", + "meta": { + "errorCode": "medications.intake.injection_site.disallowed", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "medication.intake.injection_site.disallowed" + }, + "meta": { + "medication_id": "", + "site": "ABDOMEN_RIGHT" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.11b-site-and-dose.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.11b-site-and-dose.json.snap new file mode 100644 index 000000000..8c3e32ca8 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.11b-site-and-dose.json.snap @@ -0,0 +1,840 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "idempotencyKey": null, + "scheduledFor": "2026-10-15T09:00:00.000Z", + "source": "WEB", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.intake", + "entity_type": "intake_event", + "entity_id": "" + }, + "meta": { + "medication_id": "", + "units": 0, + "item_ids": [], + "auto_opened": 0, + "skipped": false + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.12-one-shot.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.12-one-shot.json.snap new file mode 100644 index 000000000..07271f7b7 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.12-one-shot.json.snap @@ -0,0 +1,881 @@ +{ + "status": 201, + "body": { + "data": { + "id": "", + "takenAt": "2026-10-15T10:00:00.000Z", + "skipped": false, + "idempotencyKey": null, + "scheduledFor": "2026-10-15T09:00:00.000Z", + "source": "WEB", + "createdAt": "" + }, + "error": null + }, + "events": [ + { + "status": 201, + "action": { + "name": "medication.oneShot.reconciled" + }, + "meta": { + "medication_id": "", + "units": 0, + "item_ids": [], + "auto_opened": 0, + "skipped": false, + "action": "deactivate" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "queueMedicationIntakeSync", + "args": [ + { + "userId": "", + "originDeviceToken": null + } + ] + }, + { + "effect": "countOutstandingDosesToday", + "args": [ + "", + "UTC" + ] + }, + { + "effect": "notifyDelegatedIntake", + "args": [ + { + "ownerId": "", + "actorId": "", + "medicationId": "", + "status": "taken" + } + ] + }, + { + "effect": "dispatchMedicationIntakeWebClear", + "args": [ + { + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "badgeCount": 2 + } + ] + } + ], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": false, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T10:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.oneShot.reconciled", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "action": "deactivate" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.13-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.13-invalid.json.snap new file mode 100644 index 000000000..aad4622ea --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.13-invalid.json.snap @@ -0,0 +1,852 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Validation failed", + "details": { + "issues": [ + { + "path": "takenAt", + "code": "invalid_format", + "message": "Invalid ISO datetime" + }, + { + "path": "idempotencyKey", + "code": "custom", + "message": "idempotency key must be stable across app restarts; received an object description (), which embeds a memory address and is a new value on every launch — serialise the identity itself, not the object's description" + } + ] + }, + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "medications.intake.create.validation-failed" + }, + "meta": { + "issue_count": 2, + "medication_id": "", + "external_id_rejected": 1, + "external_id_shapes": "object_description", + "external_id_surface": "medication.intake.create", + "rejected_attempt": "medications.intake.create.validation-failed" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": false, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T10:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.oneShot.reconciled", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "action": "deactivate" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.14a-not-found.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.14a-not-found.json.snap new file mode 100644 index 000000000..8515c277e --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.14a-not-found.json.snap @@ -0,0 +1,829 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Medication not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": false, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T10:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.oneShot.reconciled", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "action": "deactivate" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.14b-record-only.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.14b-record-only.json.snap new file mode 100644 index 000000000..94711ec7a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.14b-record-only.json.snap @@ -0,0 +1,829 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "This medication is kept as a record; its intake is not tracked", + "meta": { + "errorCode": "medication.intake.notTracked", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": false, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T10:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.oneShot.reconciled", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "action": "deactivate" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.21-default.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.21-default.json.snap new file mode 100644 index 000000000..7f5fee32b --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.21-default.json.snap @@ -0,0 +1,1079 @@ +{ + "status": 200, + "body": { + "data": { + "events": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "meta": { + "total": 10, + "limit": 25, + "offset": 0 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.intake.list" + }, + "meta": { + "medication_id": "", + "total": 10, + "status": "all" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": false, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T10:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.oneShot.reconciled", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "action": "deactivate" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.22-completed-page.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.22-completed-page.json.snap new file mode 100644 index 000000000..15c13c61b --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.22-completed-page.json.snap @@ -0,0 +1,917 @@ +{ + "status": 200, + "body": { + "data": { + "events": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "meta": { + "total": 10, + "limit": 3, + "offset": 1 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.intake.list" + }, + "meta": { + "medication_id": "", + "total": 10, + "status": "completed" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": false, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T10:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.oneShot.reconciled", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "action": "deactivate" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.23a-skipped.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.23a-skipped.json.snap new file mode 100644 index 000000000..75c78f503 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.23a-skipped.json.snap @@ -0,0 +1,923 @@ +{ + "status": 200, + "body": { + "data": { + "events": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "meta": { + "total": 4, + "limit": 25, + "offset": 0 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.intake.list" + }, + "meta": { + "medication_id": "", + "total": 4, + "status": "skipped" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": false, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T10:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.oneShot.reconciled", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "action": "deactivate" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.23b-taken.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.23b-taken.json.snap new file mode 100644 index 000000000..ebf41b9e7 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.23b-taken.json.snap @@ -0,0 +1,995 @@ +{ + "status": 200, + "body": { + "data": { + "events": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "meta": { + "total": 6, + "limit": 25, + "offset": 0 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.intake.list" + }, + "meta": { + "medication_id": "", + "total": 6, + "status": "taken" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": false, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T10:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.oneShot.reconciled", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "action": "deactivate" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.24-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.24-invalid.json.snap new file mode 100644 index 000000000..01250074c --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.24-invalid.json.snap @@ -0,0 +1,849 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Validation failed", + "details": { + "issues": [ + { + "path": "limit", + "code": "invalid_type", + "message": "Invalid input: expected number, received NaN" + }, + { + "path": "status", + "code": "invalid_value", + "message": "Invalid option: expected one of \"all\"|\"taken\"|\"skipped\"|\"completed\"" + } + ] + }, + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "medications.intake.list.validation-failed" + }, + "meta": { + "issue_count": 2, + "medication_id": "", + "rejected_attempt": "medications.intake.list.validation-failed" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": false, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T10:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.oneShot.reconciled", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "action": "deactivate" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.25a-not-found.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.25a-not-found.json.snap new file mode 100644 index 000000000..8515c277e --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.25a-not-found.json.snap @@ -0,0 +1,829 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Medication not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": false, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T10:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.oneShot.reconciled", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "action": "deactivate" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.25b-reader.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.25b-reader.json.snap new file mode 100644 index 000000000..7f5fee32b --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.25b-reader.json.snap @@ -0,0 +1,1079 @@ +{ + "status": 200, + "body": { + "data": { + "events": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "meta": { + "total": 10, + "limit": 25, + "offset": 0 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "medication.intake.list" + }, + "meta": { + "medication_id": "", + "total": 10, + "status": "all" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": false, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T10:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.oneShot.reconciled", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "action": "deactivate" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/medication-intake.25c-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/medication-intake.25c-anonymous.json.snap new file mode 100644 index 000000000..31c8b1e6c --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/medication-intake.25c-anonymous.json.snap @@ -0,0 +1,832 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [], + "stored": { + "medications": [ + { + "id": "", + "userId": "", + "name": "As needed", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": true, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Record only", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": null, + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": false, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Single shot", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": false, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-15T00:00:00.000Z", + "endsOn": "2026-10-15T00:00:00.000Z", + "oneShot": true, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Twice daily", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "ORAL", + "trackInjectionSites": false, + "allowedInjectionSites": [], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + }, + { + "id": "", + "userId": "", + "name": "Weekly pen", + "dose": "10mg", + "treatmentClass": "GENERIC", + "dosesPerUnit": null, + "unitsPerDose": "1", + "active": true, + "notificationsEnabled": true, + "pausedAt": null, + "snoozedUntil": null, + "startsOn": "2026-10-01T00:00:00.000Z", + "endsOn": null, + "oneShot": false, + "asNeeded": false, + "trackIntake": true, + "autoMarkTaken": false, + "autoMarkTakenSince": null, + "deliveryForm": "INJECTION", + "trackInjectionSites": true, + "allowedInjectionSites": [ + "ABDOMEN_LEFT", + "ABDOMEN_RIGHT" + ], + "liveActivityEnabled": false, + "criticalAlarmEnabled": false, + "atcCode": null, + "rxNormCode": null, + "lowStockNotifiedAt": null, + "lowStockNotifiedThresholdDays": null, + "reorderLeadDays": null, + "externalSource": null, + "externalId": null, + "createdAt": "", + "updatedAt": "" + } + ], + "schedules": [ + { + "id": "", + "medicationId": "", + "windowStart": "07:00", + "windowEnd": "08:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "07:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "08:00", + "windowEnd": "10:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "08:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "09:00", + "windowEnd": "11:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "09:00" + ], + "reminderGraceMinutes": null, + "rrule": null, + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + }, + { + "id": "", + "medicationId": "", + "windowStart": "20:00", + "windowEnd": "22:00", + "label": null, + "dose": null, + "unitsPerDose": null, + "daysOfWeek": null, + "timesOfDay": [ + "20:00" + ], + "reminderGraceMinutes": null, + "rrule": "FREQ=DAILY", + "rollingIntervalDays": null, + "scheduleType": "SCHEDULED", + "cyclicOnWeeks": null, + "cyclicOffWeeks": null, + "doseWindows": null + } + ], + "revisions": [], + "pauseEras": [], + "courses": [], + "intakes": [ + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T08:00:00.000Z", + "takenAt": "2026-10-11T08:20:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-11T20:00:00.000Z", + "takenAt": "2026-10-11T20:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "API", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T08:00:00.000Z", + "takenAt": "2026-10-12T08:15:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-12T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T08:00:00.000Z", + "takenAt": "2026-10-15T11:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-13T20:00:00.000Z", + "takenAt": "2026-10-13T20:30:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": "dose-key-0001", + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T08:00:00.000Z", + "takenAt": "2026-10-14T11:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "USER_PIN", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [ + { + "units": 1, + "itemId": "" + } + ], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-14T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T08:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "REMINDER", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T09:10:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": "ABDOMEN_LEFT", + "doseTaken": "0.5 pen", + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T09:00:00.000Z", + "takenAt": "2026-10-15T10:00:00.000Z", + "skipped": false, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": [], + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T11:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + }, + { + "id": "", + "userId": "", + "medicationId": "", + "scheduledFor": "2026-10-15T20:00:00.000Z", + "takenAt": null, + "skipped": true, + "autoMissed": false, + "autoRecorded": false, + "attributionSource": "AUTO", + "source": "WEB", + "idempotencyKey": null, + "createdAt": "", + "injectionSite": null, + "doseTaken": null, + "inventoryConsumption": null, + "externalId": null, + "updatedAt": "", + "syncVersion": "", + "deletedAt": null + } + ], + "doseChanges": [], + "inventoryItems": [ + { + "id": "", + "userId": "", + "medicationId": "", + "state": "IN_USE", + "containerType": "OTHER", + "unitsTotal": "30", + "unitsRemaining": "24", + "firstUseAt": "2026-10-15T08:30:00.000Z", + "expiresAt": null, + "printedExpiry": null, + "purchasedAt": "2026-10-01T06:00:00.000Z", + "manufacturer": null, + "doseStrength": null, + "notes": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + ], + "inventoryEvents": [], + "categories": [], + "ingredients": [], + "audit": [ + { + "action": "medication.intake", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": false + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.intake", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "eventId": "", + "skipped": true + }, + "ipAddress": null + }, + { + "action": "medication.oneShot.reconciled", + "userId": "", + "actorUserId": null, + "details": { + "medicationId": "", + "action": "deactivate" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + }, + { + "action": "medications.intake.update.refused", + "userId": "", + "actorUserId": "", + "details": { + "medicationId": "", + "reason": "resolved_event" + }, + "ipAddress": null + } + ], + "idempotency": [ + { + "userId": "", + "key": "", + "method": "POST", + "path": "/api/medications//intake", + "responseStatus": 201, + "expiresAt": "2026-10-16T12:00:00.000Z" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.01-first-drain.json.snap b/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.01-first-drain.json.snap new file mode 100644 index 000000000..a87310c20 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.01-first-drain.json.snap @@ -0,0 +1,224 @@ +{ + "status": 200, + "body": { + "data": { + "processed": 7, + "inserted": 4, + "duplicates": 0, + "skipped": [ + { + "index": 3, + "reason": "Rating 3 for factor \"factor_parity_short\" is outside its scale 1..2" + }, + { + "index": 5, + "reason": "invalid_dimensions" + }, + { + "index": 6, + "reason": "unstable_external_id" + } + ], + "entries": [ + { + "index": 0, + "status": "inserted", + "id": "", + "externalId": "sd-1", + "droppedTagKeys": [ + "no_such_tag" + ], + "droppedFactorKeys": [ + "" + ] + }, + { + "index": 1, + "status": "inserted", + "id": "" + }, + { + "index": 2, + "status": "inserted", + "id": "", + "externalId": "sd-2" + }, + { + "index": 3, + "status": "skipped", + "reason": "Rating 3 for factor \"factor_parity_short\" is outside its scale 1..2", + "externalId": "sd-3" + }, + { + "index": 4, + "status": "inserted", + "id": "", + "externalId": "sd-4" + }, + { + "index": 5, + "status": "skipped", + "reason": "invalid_dimensions", + "externalId": "sd-5" + }, + { + "index": 6, + "status": "skipped", + "reason": "unstable_external_id", + "externalId": "" + } + ] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "mood.bulk.ingest" + }, + "meta": { + "write_bucket": "own", + "processed": 7, + "inserted": 4, + "duplicates": 0, + "skipped": 3, + "dropped_tag_keys": 1, + "dropped_factor_keys": 1, + "external_id_rejected": 1, + "external_id_shapes": "object_description", + "external_id_surface": "mood.bulk", + "sync_checkpoint_at": "" + }, + "warnings": null, + "error": null + } + ], + "stored": { + "entries": [ + { + "id": "", + "date": "2026-10-12", + "tz": "Europe/Berlin", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": "sd-4", + "moodLoggedAt": "2026-10-12T10:00:00.000Z", + "deletedAt": null, + "tagLinks": [], + "context": null + }, + { + "id": "", + "date": "2026-10-13", + "tz": "Europe/Berlin", + "mood": "SCHLECHT", + "score": 2, + "moodA1": 3, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": "sd-2", + "moodLoggedAt": "2026-10-13T21:00:00.000Z", + "deletedAt": null, + "tagLinks": [], + "context": { + "moodEntryId": "", + "userId": "", + "workStatus": "regular", + "workMinutes": 480, + "overtimeMinutes": null, + "workLoad": null, + "workSatisfaction": null, + "contactCircles": null, + "contactForm": null, + "contactExtent": null, + "contactQuality": null, + "contactSupport": null, + "leisureCategories": null, + "leisureMinutes": null, + "leisureJoy": null, + "leisureRecovery": null, + "eventType": null, + "eventValence": null, + "eventAt": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + }, + { + "id": "", + "date": "2026-10-14", + "tz": "Europe/Berlin", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "stressA2": 7, + "energyA3": 4, + "connectionA4": null, + "stabilityA5": null, + "tags": "[\"slept well\"]", + "note": null, + "noteEncrypted": "", + "source": "WEB", + "externalId": "sd-1", + "moodLoggedAt": "2026-10-14T07:00:00.000Z", + "deletedAt": null, + "tagLinks": [ + { + "moodTag": { + "key": "alcohol" + }, + "rating": null + }, + { + "moodTag": { + "key": "factor_parity_short" + }, + "rating": 1 + } + ], + "context": null + }, + { + "id": "", + "date": "2026-10-14", + "tz": "Europe/Berlin", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": null, + "moodLoggedAt": "2026-10-14T19:30:00.000Z", + "deletedAt": null, + "tagLinks": [], + "context": null + } + ], + "user": { + "lastSyncedAt": "" + } + } +} diff --git a/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.02-same-drain.json.snap b/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.02-same-drain.json.snap new file mode 100644 index 000000000..788dff9c3 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.02-same-drain.json.snap @@ -0,0 +1,224 @@ +{ + "status": 200, + "body": { + "data": { + "processed": 7, + "inserted": 0, + "duplicates": 4, + "skipped": [ + { + "index": 3, + "reason": "Rating 3 for factor \"factor_parity_short\" is outside its scale 1..2" + }, + { + "index": 5, + "reason": "invalid_dimensions" + }, + { + "index": 6, + "reason": "unstable_external_id" + } + ], + "entries": [ + { + "index": 0, + "status": "duplicate", + "id": "", + "externalId": "sd-1", + "droppedTagKeys": [ + "no_such_tag" + ], + "droppedFactorKeys": [ + "" + ] + }, + { + "index": 1, + "status": "duplicate", + "id": "" + }, + { + "index": 2, + "status": "duplicate", + "id": "", + "externalId": "sd-2" + }, + { + "index": 3, + "status": "skipped", + "reason": "Rating 3 for factor \"factor_parity_short\" is outside its scale 1..2", + "externalId": "sd-3" + }, + { + "index": 4, + "status": "duplicate", + "id": "", + "externalId": "sd-4" + }, + { + "index": 5, + "status": "skipped", + "reason": "invalid_dimensions", + "externalId": "sd-5" + }, + { + "index": 6, + "status": "skipped", + "reason": "unstable_external_id", + "externalId": "" + } + ] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "mood.bulk.ingest" + }, + "meta": { + "write_bucket": "own", + "processed": 7, + "inserted": 0, + "duplicates": 4, + "skipped": 3, + "dropped_tag_keys": 1, + "dropped_factor_keys": 1, + "external_id_rejected": 1, + "external_id_shapes": "object_description", + "external_id_surface": "mood.bulk", + "sync_checkpoint_at": "" + }, + "warnings": null, + "error": null + } + ], + "stored": { + "entries": [ + { + "id": "", + "date": "2026-10-12", + "tz": "Europe/Berlin", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": "sd-4", + "moodLoggedAt": "2026-10-12T10:00:00.000Z", + "deletedAt": null, + "tagLinks": [], + "context": null + }, + { + "id": "", + "date": "2026-10-13", + "tz": "Europe/Berlin", + "mood": "SCHLECHT", + "score": 2, + "moodA1": 3, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": "sd-2", + "moodLoggedAt": "2026-10-13T21:00:00.000Z", + "deletedAt": null, + "tagLinks": [], + "context": { + "moodEntryId": "", + "userId": "", + "workStatus": "regular", + "workMinutes": 480, + "overtimeMinutes": null, + "workLoad": null, + "workSatisfaction": null, + "contactCircles": null, + "contactForm": null, + "contactExtent": null, + "contactQuality": null, + "contactSupport": null, + "leisureCategories": null, + "leisureMinutes": null, + "leisureJoy": null, + "leisureRecovery": null, + "eventType": null, + "eventValence": null, + "eventAt": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + }, + { + "id": "", + "date": "2026-10-14", + "tz": "Europe/Berlin", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "stressA2": 7, + "energyA3": 4, + "connectionA4": null, + "stabilityA5": null, + "tags": "[\"slept well\"]", + "note": null, + "noteEncrypted": "", + "source": "WEB", + "externalId": "sd-1", + "moodLoggedAt": "2026-10-14T07:00:00.000Z", + "deletedAt": null, + "tagLinks": [ + { + "moodTag": { + "key": "alcohol" + }, + "rating": null + }, + { + "moodTag": { + "key": "factor_parity_short" + }, + "rating": 1 + } + ], + "context": null + }, + { + "id": "", + "date": "2026-10-14", + "tz": "Europe/Berlin", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": null, + "moodLoggedAt": "2026-10-14T19:30:00.000Z", + "deletedAt": null, + "tagLinks": [], + "context": null + } + ], + "user": { + "lastSyncedAt": "" + } + } +} diff --git a/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.03-tombstone.json.snap b/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.03-tombstone.json.snap new file mode 100644 index 000000000..dbab40614 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.03-tombstone.json.snap @@ -0,0 +1,167 @@ +{ + "status": 200, + "body": { + "data": { + "processed": 1, + "inserted": 0, + "duplicates": 1, + "skipped": [], + "entries": [ + { + "index": 0, + "status": "duplicate", + "id": "", + "externalId": "sd-2" + } + ] + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "mood.bulk.ingest" + }, + "meta": { + "write_bucket": "own", + "processed": 1, + "inserted": 0, + "duplicates": 1, + "skipped": 0, + "dropped_tag_keys": 0, + "dropped_factor_keys": 0, + "sync_checkpoint_at": "" + }, + "warnings": null, + "error": null + } + ], + "stored": { + "entries": [ + { + "id": "", + "date": "2026-10-12", + "tz": "Europe/Berlin", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": "sd-4", + "moodLoggedAt": "2026-10-12T10:00:00.000Z", + "deletedAt": null, + "tagLinks": [], + "context": null + }, + { + "id": "", + "date": "2026-10-13", + "tz": "Europe/Berlin", + "mood": "SCHLECHT", + "score": 2, + "moodA1": 3, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": "sd-2", + "moodLoggedAt": "2026-10-13T21:00:00.000Z", + "deletedAt": "2026-10-14T20:00:00.000Z", + "tagLinks": [], + "context": { + "moodEntryId": "", + "userId": "", + "workStatus": "regular", + "workMinutes": 480, + "overtimeMinutes": null, + "workLoad": null, + "workSatisfaction": null, + "contactCircles": null, + "contactForm": null, + "contactExtent": null, + "contactQuality": null, + "contactSupport": null, + "leisureCategories": null, + "leisureMinutes": null, + "leisureJoy": null, + "leisureRecovery": null, + "eventType": null, + "eventValence": null, + "eventAt": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + }, + { + "id": "", + "date": "2026-10-14", + "tz": "Europe/Berlin", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "stressA2": 7, + "energyA3": 4, + "connectionA4": null, + "stabilityA5": null, + "tags": "[\"slept well\"]", + "note": null, + "noteEncrypted": "", + "source": "WEB", + "externalId": "sd-1", + "moodLoggedAt": "2026-10-14T07:00:00.000Z", + "deletedAt": null, + "tagLinks": [ + { + "moodTag": { + "key": "alcohol" + }, + "rating": null + }, + { + "moodTag": { + "key": "factor_parity_short" + }, + "rating": 1 + } + ], + "context": null + }, + { + "id": "", + "date": "2026-10-14", + "tz": "Europe/Berlin", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": null, + "moodLoggedAt": "2026-10-14T19:30:00.000Z", + "deletedAt": null, + "tagLinks": [], + "context": null + } + ], + "user": { + "lastSyncedAt": "" + } + } +} diff --git a/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.04-too-large.json.snap b/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.04-too-large.json.snap new file mode 100644 index 000000000..d7de3214c --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.04-too-large.json.snap @@ -0,0 +1,149 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Batch exceeds the 500-entry limit", + "meta": { + "errorCode": "mood.bulk.too_large", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": { + "write_bucket": "own" + }, + "warnings": null, + "error": null + } + ], + "stored": { + "entries": [ + { + "id": "", + "date": "2026-10-12", + "tz": "Europe/Berlin", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": "sd-4", + "moodLoggedAt": "2026-10-12T10:00:00.000Z", + "deletedAt": null, + "tagLinks": [], + "context": null + }, + { + "id": "", + "date": "2026-10-13", + "tz": "Europe/Berlin", + "mood": "SCHLECHT", + "score": 2, + "moodA1": 3, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": "sd-2", + "moodLoggedAt": "2026-10-13T21:00:00.000Z", + "deletedAt": "2026-10-14T20:00:00.000Z", + "tagLinks": [], + "context": { + "moodEntryId": "", + "userId": "", + "workStatus": "regular", + "workMinutes": 480, + "overtimeMinutes": null, + "workLoad": null, + "workSatisfaction": null, + "contactCircles": null, + "contactForm": null, + "contactExtent": null, + "contactQuality": null, + "contactSupport": null, + "leisureCategories": null, + "leisureMinutes": null, + "leisureJoy": null, + "leisureRecovery": null, + "eventType": null, + "eventValence": null, + "eventAt": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + }, + { + "id": "", + "date": "2026-10-14", + "tz": "Europe/Berlin", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "stressA2": 7, + "energyA3": 4, + "connectionA4": null, + "stabilityA5": null, + "tags": "[\"slept well\"]", + "note": null, + "noteEncrypted": "", + "source": "WEB", + "externalId": "sd-1", + "moodLoggedAt": "2026-10-14T07:00:00.000Z", + "deletedAt": null, + "tagLinks": [ + { + "moodTag": { + "key": "alcohol" + }, + "rating": null + }, + { + "moodTag": { + "key": "factor_parity_short" + }, + "rating": 1 + } + ], + "context": null + }, + { + "id": "", + "date": "2026-10-14", + "tz": "Europe/Berlin", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": null, + "moodLoggedAt": "2026-10-14T19:30:00.000Z", + "deletedAt": null, + "tagLinks": [], + "context": null + } + ], + "user": { + "lastSyncedAt": "" + } + } +} diff --git a/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.05-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.05-invalid.json.snap new file mode 100644 index 000000000..a6898965c --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/mood-entries-bulk.05-invalid.json.snap @@ -0,0 +1,166 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Validation failed", + "details": { + "issues": [ + { + "path": "entries.0.mood", + "code": "invalid_value", + "message": "Invalid option: expected one of \"SUPER_GUT\"|\"GUT\"|\"OKAY\"|\"SCHLECHT\"|\"LAUSIG\"" + }, + { + "path": "entries.0.moodLoggedAt", + "code": "invalid_type", + "message": "Invalid input: expected string, received undefined" + } + ] + }, + "meta": { + "errorCode": "mood.bulk.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "mood.bulk.validation-failed" + }, + "meta": { + "write_bucket": "own", + "issue_count": 2 + }, + "warnings": null, + "error": null + } + ], + "stored": { + "entries": [ + { + "id": "", + "date": "2026-10-12", + "tz": "Europe/Berlin", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": "sd-4", + "moodLoggedAt": "2026-10-12T10:00:00.000Z", + "deletedAt": null, + "tagLinks": [], + "context": null + }, + { + "id": "", + "date": "2026-10-13", + "tz": "Europe/Berlin", + "mood": "SCHLECHT", + "score": 2, + "moodA1": 3, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": "sd-2", + "moodLoggedAt": "2026-10-13T21:00:00.000Z", + "deletedAt": "2026-10-14T20:00:00.000Z", + "tagLinks": [], + "context": { + "moodEntryId": "", + "userId": "", + "workStatus": "regular", + "workMinutes": 480, + "overtimeMinutes": null, + "workLoad": null, + "workSatisfaction": null, + "contactCircles": null, + "contactForm": null, + "contactExtent": null, + "contactQuality": null, + "contactSupport": null, + "leisureCategories": null, + "leisureMinutes": null, + "leisureJoy": null, + "leisureRecovery": null, + "eventType": null, + "eventValence": null, + "eventAt": null, + "notesEncrypted": null, + "createdAt": "", + "updatedAt": "" + } + }, + { + "id": "", + "date": "2026-10-14", + "tz": "Europe/Berlin", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "stressA2": 7, + "energyA3": 4, + "connectionA4": null, + "stabilityA5": null, + "tags": "[\"slept well\"]", + "note": null, + "noteEncrypted": "", + "source": "WEB", + "externalId": "sd-1", + "moodLoggedAt": "2026-10-14T07:00:00.000Z", + "deletedAt": null, + "tagLinks": [ + { + "moodTag": { + "key": "alcohol" + }, + "rating": null + }, + { + "moodTag": { + "key": "factor_parity_short" + }, + "rating": 1 + } + ], + "context": null + }, + { + "id": "", + "date": "2026-10-14", + "tz": "Europe/Berlin", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "stressA2": null, + "energyA3": null, + "connectionA4": null, + "stabilityA5": null, + "tags": null, + "note": null, + "noteEncrypted": null, + "source": "MANUAL", + "externalId": null, + "moodLoggedAt": "2026-10-14T19:30:00.000Z", + "deletedAt": null, + "tagLinks": [], + "context": null + } + ], + "user": { + "lastSyncedAt": "" + } + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.01-disabled.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.01-disabled.json.snap new file mode 100644 index 000000000..1a50e2ca5 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.01-disabled.json.snap @@ -0,0 +1,26 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/login?error=oidc_disabled", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [], + "sessions": [], + "audit": [], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.02-idp-denied.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.02-idp-denied.json.snap new file mode 100644 index 000000000..68266cf05 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.02-idp-denied.json.snap @@ -0,0 +1,29 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/login?error=oidc_denied", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": { + "reason": "idp_denied", + "idpError": "access_denied" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [], + "sessions": [], + "audit": [], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.03-idp-denied-unknown-code.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.03-idp-denied-unknown-code.json.snap new file mode 100644 index 000000000..786057ee1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.03-idp-denied-unknown-code.json.snap @@ -0,0 +1,29 @@ +{ + "status": 307, + "location": "healthlog://oidc-callback?error=oidc_denied", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": { + "reason": "idp_denied", + "idpError": "other" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [], + "sessions": [], + "audit": [], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.04-no-code.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.04-no-code.json.snap new file mode 100644 index 000000000..9740fe43d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.04-no-code.json.snap @@ -0,0 +1,26 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/login?error=oidc_state", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [], + "sessions": [], + "audit": [], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.05-no-state-blob.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.05-no-state-blob.json.snap new file mode 100644 index 000000000..9740fe43d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.05-no-state-blob.json.snap @@ -0,0 +1,26 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/login?error=oidc_state", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [], + "sessions": [], + "audit": [], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.06-forged-state.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.06-forged-state.json.snap new file mode 100644 index 000000000..442de779d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.06-forged-state.json.snap @@ -0,0 +1,28 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/login?error=oidc_state", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": { + "reason": "csrf" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [], + "sessions": [], + "audit": [], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.07-exchange-failed.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.07-exchange-failed.json.snap new file mode 100644 index 000000000..d8cdaf88e --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.07-exchange-failed.json.snap @@ -0,0 +1,31 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/login?error=oidc_failed", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": { + "reason": "exchange_or_verify_failed" + }, + "warnings": null, + "error": { + "type": "Error", + "message": "invalid_grant" + } + } + ], + "effects": [], + "stored": { + "users": [], + "sessions": [], + "audit": [], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.08-no-id-token.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.08-no-id-token.json.snap new file mode 100644 index 000000000..68261b197 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.08-no-id-token.json.snap @@ -0,0 +1,26 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/login?error=oidc_failed", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [], + "sessions": [], + "audit": [], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.09-provision-first.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.09-provision-first.json.snap new file mode 100644 index 000000000..b5bd1bda6 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.09-provision-first.json.snap @@ -0,0 +1,66 @@ +{ + "status": 307, + "location": "http://localhost:3000/insights", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "recordSignInDevice", + "args": [ + { + "userId": "", + "ip": "unknown", + "userAgent": null + } + ] + } + ], + "stored": { + "users": [ + { + "username": "First", + "email": "first@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + } + ], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.10-provision-second.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.10-provision-second.json.snap new file mode 100644 index 000000000..c0b84257d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.10-provision-second.json.snap @@ -0,0 +1,92 @@ +{ + "status": 307, + "location": "http://localhost:3000/", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "recordSignInDevice", + "args": [ + { + "userId": "", + "ip": "unknown", + "userAgent": null + } + ] + } + ], + "stored": { + "users": [ + { + "username": "First", + "email": "first@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + } + ], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.11-returning-email-refresh.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.11-returning-email-refresh.json.snap new file mode 100644 index 000000000..ac180d4b5 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.11-returning-email-refresh.json.snap @@ -0,0 +1,104 @@ +{ + "status": 307, + "location": "http://localhost:3000/", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "recordSignInDevice", + "args": [ + { + "userId": "", + "ip": "unknown", + "userAgent": null + } + ] + } + ], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + } + ], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.12-duplicate-with-session.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.12-duplicate-with-session.json.snap new file mode 100644 index 000000000..24d839ad7 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.12-duplicate-with-session.json.snap @@ -0,0 +1,101 @@ +{ + "status": 307, + "location": "http://localhost:3000/", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": { + "reason": "duplicate_callback_authenticated" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + } + ], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.13-no-email.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.13-no-email.json.snap new file mode 100644 index 000000000..08674b8a7 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.13-no-email.json.snap @@ -0,0 +1,101 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/login?error=oidc_no_email", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": { + "reason": "no_email" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + } + ], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.14-email-unverified.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.14-email-unverified.json.snap new file mode 100644 index 000000000..5d380be3a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.14-email-unverified.json.snap @@ -0,0 +1,101 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/login?error=oidc_email_unverified", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": { + "reason": "email_unverified" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + } + ], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.15-userinfo-provision.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.15-userinfo-provision.json.snap new file mode 100644 index 000000000..f0c4ffb58 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.15-userinfo-provision.json.snap @@ -0,0 +1,136 @@ +{ + "status": 307, + "location": "http://localhost:3000/", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "recordSignInDevice", + "args": [ + { + "userId": "", + "ip": "unknown", + "userAgent": null + } + ] + } + ], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + }, + { + "username": "userinfo", + "email": "userinfo@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-userinfo" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "userinfo" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + } + ], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.16-link-conflict.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.16-link-conflict.json.snap new file mode 100644 index 000000000..ddbf29429 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.16-link-conflict.json.snap @@ -0,0 +1,141 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/login?error=oidc_identity_conflict", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": { + "reason": "identity_conflict" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "linked-elsewhere", + "email": "conflict@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://other.example.test", + "oidcSub": "other-sub" + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + }, + { + "username": "userinfo", + "email": "userinfo@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-userinfo" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "userinfo" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.oidc.link_conflict", + "user": { + "username": "linked-elsewhere" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + } + ], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.17-link-pending-web.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.17-link-pending-web.json.snap new file mode 100644 index 000000000..e00f75079 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.17-link-pending-web.json.snap @@ -0,0 +1,156 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/login?error=oidc_link_required", + "setCookies": [ + "hl_oidc_link=; Path=/api/auth; Expires=; Max-Age=600; HttpOnly; SameSite=lax", + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": { + "reason": "link_confirmation_required" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "linked-elsewhere", + "email": "conflict@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://other.example.test", + "oidcSub": "other-sub" + }, + { + "username": "local-only", + "email": "local@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": null, + "oidcSub": null + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + }, + { + "username": "userinfo", + "email": "userinfo@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-userinfo" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "userinfo" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.oidc.link_conflict", + "user": { + "username": "linked-elsewhere" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "userinfo" + } + } + ], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.18-link-pending-native.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.18-link-pending-native.json.snap new file mode 100644 index 000000000..5aa88c3a7 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.18-link-pending-native.json.snap @@ -0,0 +1,161 @@ +{ + "status": 307, + "location": "healthlog://oidc-callback?error=oidc_link_required", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": { + "reason": "link_confirmation_required" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "linked-elsewhere", + "email": "conflict@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://other.example.test", + "oidcSub": "other-sub" + }, + { + "username": "local-only", + "email": "local@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": null, + "oidcSub": null + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + }, + { + "username": "userinfo", + "email": "userinfo@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-userinfo" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "userinfo" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.oidc.link_conflict", + "user": { + "username": "linked-elsewhere" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + } + ], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.19-registration-closed.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.19-registration-closed.json.snap new file mode 100644 index 000000000..9914dceb7 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.19-registration-closed.json.snap @@ -0,0 +1,161 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/login?error=oidc_registration_disabled", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": { + "reason": "registration_disabled" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "linked-elsewhere", + "email": "conflict@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://other.example.test", + "oidcSub": "other-sub" + }, + { + "username": "local-only", + "email": "local@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": null, + "oidcSub": null + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + }, + { + "username": "userinfo", + "email": "userinfo@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-userinfo" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "userinfo" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.oidc.link_conflict", + "user": { + "username": "linked-elsewhere" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + } + ], + "challenges": 0, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.20-mfa-web.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.20-mfa-web.json.snap new file mode 100644 index 000000000..539539bda --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.20-mfa-web.json.snap @@ -0,0 +1,168 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/login?next=%2Fmedications", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=", + "oidc_mfa_handoff=; Path=/auth/login; Expires=; Max-Age=300; SameSite=lax" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.mfa.challenge" + }, + "meta": { + "mfa_required": true + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "linked-elsewhere", + "email": "conflict@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://other.example.test", + "oidcSub": "other-sub" + }, + { + "username": "local-only", + "email": "local@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": null, + "oidcSub": null + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + }, + { + "username": "userinfo", + "email": "userinfo@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-userinfo" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "userinfo" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.mfa.challenge", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.link_conflict", + "user": { + "username": "linked-elsewhere" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + } + ], + "challenges": 1, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.21-mfa-native.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.21-mfa-native.json.snap new file mode 100644 index 000000000..39d982593 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.21-mfa-native.json.snap @@ -0,0 +1,173 @@ +{ + "status": 307, + "location": "healthlog://oidc-callback?mfa_ticket=&methods=totp%2Crecovery", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.mfa.challenge" + }, + "meta": { + "mfa_required": true + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "linked-elsewhere", + "email": "conflict@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://other.example.test", + "oidcSub": "other-sub" + }, + { + "username": "local-only", + "email": "local@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": null, + "oidcSub": null + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + }, + { + "username": "userinfo", + "email": "userinfo@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-userinfo" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "userinfo" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.mfa.challenge", + "user": { + "username": "second" + } + }, + { + "action": "auth.mfa.challenge", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.link_conflict", + "user": { + "username": "linked-elsewhere" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + } + ], + "challenges": 2, + "handoffs": 0 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.22-native-handoff.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.22-native-handoff.json.snap new file mode 100644 index 000000000..bf9726436 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.22-native-handoff.json.snap @@ -0,0 +1,177 @@ +{ + "status": 307, + "location": "healthlog://oidc-callback?code=", + "setCookies": [ + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.native.handoff_minted" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "linked-elsewhere", + "email": "conflict@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://other.example.test", + "oidcSub": "other-sub" + }, + { + "username": "local-only", + "email": "local@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": null, + "oidcSub": null + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + }, + { + "username": "userinfo", + "email": "userinfo@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-userinfo" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "userinfo" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.mfa.challenge", + "user": { + "username": "second" + } + }, + { + "action": "auth.mfa.challenge", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.link_conflict", + "user": { + "username": "linked-elsewhere" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.native.handoff_minted", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + } + ], + "challenges": 2, + "handoffs": 1 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.23-claim-signed-in.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.23-claim-signed-in.json.snap new file mode 100644 index 000000000..bde6ddcce --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.23-claim-signed-in.json.snap @@ -0,0 +1,186 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/claim?error=claim_signed_in", + "setCookies": [ + "hl_claim=; Path=/api/auth/oidc; Expires=", + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": { + "reason": "claim_signed_in" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "linked-elsewhere", + "email": "conflict@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://other.example.test", + "oidcSub": "other-sub" + }, + { + "username": "local-only", + "email": "local@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": null, + "oidcSub": null + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + }, + { + "username": "userinfo", + "email": "userinfo@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-userinfo" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "userinfo" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.mfa.challenge", + "user": { + "username": "second" + } + }, + { + "action": "auth.mfa.challenge", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.link_conflict", + "user": { + "username": "linked-elsewhere" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.native.handoff_minted", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + } + ], + "challenges": 2, + "handoffs": 1 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.24-claim-no-cookie.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.24-claim-no-cookie.json.snap new file mode 100644 index 000000000..013e26ef7 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.24-claim-no-cookie.json.snap @@ -0,0 +1,186 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/claim?error=claim_expired", + "setCookies": [ + "hl_claim=; Path=/api/auth/oidc; Expires=", + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": { + "reason": "claim_expired" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "linked-elsewhere", + "email": "conflict@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://other.example.test", + "oidcSub": "other-sub" + }, + { + "username": "local-only", + "email": "local@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": null, + "oidcSub": null + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + }, + { + "username": "userinfo", + "email": "userinfo@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-userinfo" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "userinfo" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.mfa.challenge", + "user": { + "username": "second" + } + }, + { + "action": "auth.mfa.challenge", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.link_conflict", + "user": { + "username": "linked-elsewhere" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.native.handoff_minted", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + } + ], + "challenges": 2, + "handoffs": 1 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/oidc-callback.25-claim-no-code.json.snap b/tests/integration/routes/parity/__snapshots__/oidc-callback.25-claim-no-code.json.snap new file mode 100644 index 000000000..80dbd0772 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/oidc-callback.25-claim-no-code.json.snap @@ -0,0 +1,184 @@ +{ + "status": 307, + "location": "http://localhost:3000/auth/claim?error=oidc_state", + "setCookies": [ + "hl_claim=; Path=/api/auth/oidc; Expires=", + "oidc_auth_state=; Path=/api/auth/oidc; Expires=" + ], + "events": [ + { + "status": 307, + "action": { + "name": "auth.oidc.callback" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "users": [ + { + "username": "First", + "email": "first.new@example.test", + "role": "ADMIN", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-first" + }, + { + "username": "linked-elsewhere", + "email": "conflict@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://other.example.test", + "oidcSub": "other-sub" + }, + { + "username": "local-only", + "email": "local@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": null, + "oidcSub": null + }, + { + "username": "second", + "email": "second@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-second" + }, + { + "username": "userinfo", + "email": "userinfo@example.test", + "role": "USER", + "timezone": "Europe/Berlin", + "oidcIssuer": "https://id.example.test", + "oidcSub": "sub-userinfo" + } + ], + "sessions": [ + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "First" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "second" + }, + "mfaVerifiedAt": null + }, + { + "user": { + "username": "userinfo" + }, + "mfaVerifiedAt": null + } + ], + "audit": [ + { + "action": "auth.mfa.challenge", + "user": { + "username": "second" + } + }, + { + "action": "auth.mfa.challenge", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.link_conflict", + "user": { + "username": "linked-elsewhere" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.link_pending", + "user": { + "username": "local-only" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.login", + "user": { + "username": "First" + } + }, + { + "action": "auth.oidc.native.handoff_minted", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "userinfo" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "second" + } + }, + { + "action": "auth.oidc.provisioned", + "user": { + "username": "First" + } + } + ], + "challenges": 2, + "handoffs": 1 + } +} diff --git a/tests/integration/routes/parity/__snapshots__/sync-changes.01-first-page.json.snap b/tests/integration/routes/parity/__snapshots__/sync-changes.01-first-page.json.snap new file mode 100644 index 000000000..cac388d58 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/sync-changes.01-first-page.json.snap @@ -0,0 +1,230 @@ +{ + "status": 200, + "body": { + "data": { + "serverNow": "", + "cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzkyMDQ4MjAwMDAwLCJpIjoibS0wMiJ9LCJtb29kIjp7InUiOjE3OTIwNTE4MDAwMDAsImkiOiJtb29kLTAyIn0sImludGFrZXMiOnsidSI6MTc5MjA1MzAwMDAwMCwiaSI6ImluLTAyIn0sImN5Y2xlRGF5cyI6eyJ1IjoxNzkyMDU2NjAwMDAwLCJpIjoiY2QtMDIifSwiY3ljbGVzIjp7InUiOjE3OTIwNTQ4MDAwMDAsImkiOiJjeS0wMiJ9fX0", + "hasMore": true, + "cursorExpired": false, + "changes": { + "measurements": { + "upserts": [ + { + "id": "m-01", + "externalId": null, + "type": "WEIGHT", + "value": 81.2, + "unit": "kg", + "measuredAt": "2026-10-14T06:30:00.000Z", + "source": "MANUAL", + "notes": "after the run", + "syncVersion": "", + "updatedAt": "" + }, + { + "id": "m-02", + "externalId": "w-pulse-1", + "type": "PULSE", + "value": 61, + "unit": "bpm", + "measuredAt": "2026-10-14T07:00:00.000Z", + "source": "WITHINGS", + "notes": null, + "syncVersion": "", + "updatedAt": "" + } + ], + "tombstones": [] + }, + "mood": { + "upserts": [ + { + "id": "mood-01", + "date": "2026-10-14", + "mood": "GUT", + "score": 4, + "a1": 7, + "a2": 3, + "a3": null, + "a4": null, + "a5": null, + "context": { + "workStatus": "regular", + "workMinutes": 480, + "overtimeMinutes": null, + "workLoad": null, + "workSatisfaction": null, + "contactCircles": [], + "contactForm": null, + "contactExtent": null, + "contactQuality": null, + "contactSupport": null, + "leisureCategories": [], + "leisureMinutes": null, + "leisureJoy": null, + "leisureRecovery": null, + "eventType": null, + "eventValence": null, + "eventAt": null, + "note": null + }, + "tags": "[\"slept well\"]", + "note": "a calm morning", + "moodLoggedAt": "2026-10-14T07:00:00.000Z", + "source": "MANUAL", + "syncVersion": "", + "updatedAt": "" + } + ], + "tombstones": [ + { + "id": "mood-02", + "syncVersion": "", + "deletedAt": "2026-10-15T11:30:00.000Z", + "updatedAt": "" + } + ] + }, + "intakes": { + "upserts": [ + { + "id": "in-01", + "medicationId": "", + "scheduledFor": "2026-10-14T06:00:00.000Z", + "takenAt": "2026-10-14T06:10:00.000Z", + "skipped": false, + "source": "WEB", + "autoRecorded": false, + "syncVersion": "", + "updatedAt": "" + }, + { + "id": "in-02", + "medicationId": "", + "scheduledFor": "2026-10-13T06:00:00.000Z", + "takenAt": "2026-10-13T06:00:00.000Z", + "skipped": false, + "source": "WEB", + "autoRecorded": true, + "syncVersion": "", + "updatedAt": "" + } + ], + "tombstones": [] + }, + "cycleDays": { + "upserts": [ + { + "id": "cd-01", + "date": "2026-10-10", + "cycleId": "cy-02", + "flow": "MEDIUM", + "intermenstrualBleeding": false, + "basalBodyTempC": null, + "temperatureExcluded": false, + "ovulationTest": null, + "cervicalMucus": null, + "cervixPosition": null, + "cervixFirmness": null, + "cervixOpening": null, + "sexualActivity": false, + "protectedSex": null, + "pregnancyTest": null, + "progesteroneTest": null, + "contraceptive": null, + "symptoms": [], + "note": null, + "source": "MANUAL", + "externalId": null, + "syncVersion": "", + "updatedAt": "", + "deletedAt": null + }, + { + "id": "cd-02", + "date": "2026-10-11", + "cycleId": "cy-02", + "flow": "LIGHT", + "intermenstrualBleeding": false, + "basalBodyTempC": 36.4, + "temperatureExcluded": false, + "ovulationTest": null, + "cervicalMucus": null, + "cervixPosition": null, + "cervixFirmness": null, + "cervixOpening": null, + "sexualActivity": false, + "protectedSex": null, + "pregnancyTest": null, + "progesteroneTest": null, + "contraceptive": null, + "symptoms": [], + "note": null, + "source": "MANUAL", + "externalId": "hk-cycle-1", + "syncVersion": "", + "updatedAt": "", + "deletedAt": null + } + ], + "tombstones": [] + }, + "cycles": { + "upserts": [ + { + "id": "cy-01", + "startDate": "2026-09-12", + "endDate": "2026-10-09", + "periodEndDate": "2026-09-16", + "lengthDays": 28, + "ovulationDate": null, + "ovulationConfirmed": false, + "isPredicted": false, + "syncVersion": "", + "updatedAt": "" + }, + { + "id": "cy-02", + "startDate": "2026-10-10", + "endDate": null, + "periodEndDate": null, + "lengthDays": null, + "ovulationDate": null, + "ovulationConfirmed": false, + "isPredicted": false, + "syncVersion": "", + "updatedAt": "" + } + ], + "tombstones": [] + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "sync.changes.pull" + }, + "meta": { + "measurement_upserts": 2, + "measurement_tombstones": 0, + "mood_upserts": 1, + "mood_tombstones": 1, + "intake_upserts": 2, + "intake_tombstones": 0, + "cycle_day_upserts": 2, + "cycle_day_tombstones": 0, + "cycle_upserts": 2, + "cycle_tombstones": 0, + "cycle_module_enabled": true, + "has_more": true, + "cursor_present": false + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/sync-changes.02-second-page.json.snap b/tests/integration/routes/parity/__snapshots__/sync-changes.02-second-page.json.snap new file mode 100644 index 000000000..96ebe0b69 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/sync-changes.02-second-page.json.snap @@ -0,0 +1,102 @@ +{ + "status": 200, + "body": { + "data": { + "serverNow": "", + "cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzkyMDQ5NDAwMDAwLCJpIjoibS0wNCJ9LCJtb29kIjp7InUiOjE3OTIwNTE4MDAwMDAsImkiOiJtb29kLTAyIn0sImludGFrZXMiOnsidSI6MTc5MjA1MzYwMDAwMCwiaSI6ImluLTAzIn0sImN5Y2xlRGF5cyI6eyJ1IjoxNzkyMDU3MjAwMDAwLCJpIjoiY2QtMDMifSwiY3ljbGVzIjp7InUiOjE3OTIwNTU0MDAwMDAsImkiOiJjeS0wMyJ9fX0", + "hasMore": true, + "cursorExpired": false, + "changes": { + "measurements": { + "upserts": [ + { + "id": "m-04", + "externalId": "stats:HKQuantityTypeIdentifierHeartRate:2026-06-17T09", + "type": "PULSE", + "value": 64, + "unit": "bpm", + "measuredAt": "2026-06-17T09:30:00.000Z", + "source": "APPLE_HEALTH", + "notes": null, + "syncVersion": "", + "updatedAt": "" + } + ], + "tombstones": [ + { + "id": "m-03", + "externalId": "w-weight-2", + "syncVersion": "", + "deletedAt": "2026-10-15T11:30:00.000Z", + "updatedAt": "" + } + ] + }, + "mood": { + "upserts": [], + "tombstones": [] + }, + "intakes": { + "upserts": [], + "tombstones": [ + { + "id": "in-03", + "syncVersion": "", + "deletedAt": "2026-10-15T11:30:00.000Z", + "updatedAt": "" + } + ] + }, + "cycleDays": { + "upserts": [], + "tombstones": [ + { + "id": "cd-03", + "externalId": "hk-cycle-0", + "syncVersion": "", + "deletedAt": "2026-10-15T11:30:00.000Z", + "updatedAt": "" + } + ] + }, + "cycles": { + "upserts": [], + "tombstones": [ + { + "id": "cy-03", + "syncVersion": "", + "deletedAt": "2026-10-15T11:30:00.000Z", + "updatedAt": "" + } + ] + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "sync.changes.pull" + }, + "meta": { + "measurement_upserts": 1, + "measurement_tombstones": 1, + "mood_upserts": 0, + "mood_tombstones": 0, + "intake_upserts": 0, + "intake_tombstones": 1, + "cycle_day_upserts": 0, + "cycle_day_tombstones": 1, + "cycle_upserts": 0, + "cycle_tombstones": 1, + "cycle_module_enabled": true, + "has_more": true, + "cursor_present": true + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/sync-changes.03-the-rest.json.snap b/tests/integration/routes/parity/__snapshots__/sync-changes.03-the-rest.json.snap new file mode 100644 index 000000000..88e5797a6 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/sync-changes.03-the-rest.json.snap @@ -0,0 +1,72 @@ +{ + "status": 200, + "body": { + "data": { + "serverNow": "", + "cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzkyMDUwNjAwMDAwLCJpIjoibS0wNiJ9LCJtb29kIjp7InUiOjE3OTIwNTE4MDAwMDAsImkiOiJtb29kLTAyIn0sImludGFrZXMiOnsidSI6MTc5MjA1MzYwMDAwMCwiaSI6ImluLTAzIn0sImN5Y2xlRGF5cyI6eyJ1IjoxNzkyMDU3MjAwMDAwLCJpIjoiY2QtMDMifSwiY3ljbGVzIjp7InUiOjE3OTIwNTU0MDAwMDAsImkiOiJjeS0wMyJ9fX0", + "hasMore": false, + "cursorExpired": false, + "changes": { + "measurements": { + "upserts": [ + { + "id": "m-06", + "externalId": null, + "type": "BLOOD_PRESSURE_SYS", + "value": 128, + "unit": "mmHg", + "measuredAt": "2026-10-14T08:00:00.000Z", + "source": "MANUAL", + "notes": null, + "syncVersion": "", + "updatedAt": "" + } + ], + "tombstones": [] + }, + "mood": { + "upserts": [], + "tombstones": [] + }, + "intakes": { + "upserts": [], + "tombstones": [] + }, + "cycleDays": { + "upserts": [], + "tombstones": [] + }, + "cycles": { + "upserts": [], + "tombstones": [] + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "sync.changes.pull" + }, + "meta": { + "measurement_upserts": 1, + "measurement_tombstones": 0, + "mood_upserts": 0, + "mood_tombstones": 0, + "intake_upserts": 0, + "intake_tombstones": 0, + "cycle_day_upserts": 0, + "cycle_day_tombstones": 0, + "cycle_upserts": 0, + "cycle_tombstones": 0, + "cycle_module_enabled": true, + "has_more": false, + "cursor_present": true + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/sync-changes.04-caught-up.json.snap b/tests/integration/routes/parity/__snapshots__/sync-changes.04-caught-up.json.snap new file mode 100644 index 000000000..d50f7efae --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/sync-changes.04-caught-up.json.snap @@ -0,0 +1,59 @@ +{ + "status": 200, + "body": { + "data": { + "serverNow": "", + "cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzkyMDUwNjAwMDAwLCJpIjoibS0wNiJ9LCJtb29kIjp7InUiOjE3OTIwNTE4MDAwMDAsImkiOiJtb29kLTAyIn0sImludGFrZXMiOnsidSI6MTc5MjA1MzYwMDAwMCwiaSI6ImluLTAzIn0sImN5Y2xlRGF5cyI6eyJ1IjoxNzkyMDU3MjAwMDAwLCJpIjoiY2QtMDMifSwiY3ljbGVzIjp7InUiOjE3OTIwNTU0MDAwMDAsImkiOiJjeS0wMyJ9fX0", + "hasMore": false, + "cursorExpired": false, + "changes": { + "measurements": { + "upserts": [], + "tombstones": [] + }, + "mood": { + "upserts": [], + "tombstones": [] + }, + "intakes": { + "upserts": [], + "tombstones": [] + }, + "cycleDays": { + "upserts": [], + "tombstones": [] + }, + "cycles": { + "upserts": [], + "tombstones": [] + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "sync.changes.pull" + }, + "meta": { + "measurement_upserts": 0, + "measurement_tombstones": 0, + "mood_upserts": 0, + "mood_tombstones": 0, + "intake_upserts": 0, + "intake_tombstones": 0, + "cycle_day_upserts": 0, + "cycle_day_tombstones": 0, + "cycle_upserts": 0, + "cycle_tombstones": 0, + "cycle_module_enabled": true, + "has_more": false, + "cursor_present": true + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/sync-changes.05-past-retention.json.snap b/tests/integration/routes/parity/__snapshots__/sync-changes.05-past-retention.json.snap new file mode 100644 index 000000000..06894579d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/sync-changes.05-past-retention.json.snap @@ -0,0 +1,49 @@ +{ + "status": 200, + "body": { + "data": { + "serverNow": "", + "cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzc0Nzg1NjAwMDAwLCJpIjoibS0wMSJ9fX0", + "hasMore": false, + "cursorExpired": true, + "changes": { + "measurements": { + "upserts": [], + "tombstones": [] + }, + "mood": { + "upserts": [], + "tombstones": [] + }, + "intakes": { + "upserts": [], + "tombstones": [] + }, + "cycleDays": { + "upserts": [], + "tombstones": [] + }, + "cycles": { + "upserts": [], + "tombstones": [] + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "sync.changes.pull" + }, + "meta": { + "cursor_expired": true, + "cursor_expired_reason": "retention", + "returned": 0 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/sync-changes.06-before-reset.json.snap b/tests/integration/routes/parity/__snapshots__/sync-changes.06-before-reset.json.snap new file mode 100644 index 000000000..360c66c24 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/sync-changes.06-before-reset.json.snap @@ -0,0 +1,49 @@ +{ + "status": 200, + "body": { + "data": { + "serverNow": "", + "cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzkyMDQ4MjAwMDAwLCJpIjoibS0wMiJ9LCJtb29kIjp7InUiOjE3OTIwNTE4MDAwMDAsImkiOiJtb29kLTAyIn0sImludGFrZXMiOnsidSI6MTc5MjA1MzAwMDAwMCwiaSI6ImluLTAyIn0sImN5Y2xlRGF5cyI6eyJ1IjoxNzkyMDU2NjAwMDAwLCJpIjoiY2QtMDIifSwiY3ljbGVzIjp7InUiOjE3OTIwNTQ4MDAwMDAsImkiOiJjeS0wMiJ9fX0", + "hasMore": false, + "cursorExpired": true, + "changes": { + "measurements": { + "upserts": [], + "tombstones": [] + }, + "mood": { + "upserts": [], + "tombstones": [] + }, + "intakes": { + "upserts": [], + "tombstones": [] + }, + "cycleDays": { + "upserts": [], + "tombstones": [] + }, + "cycles": { + "upserts": [], + "tombstones": [] + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "sync.changes.pull" + }, + "meta": { + "cursor_expired": true, + "cursor_expired_reason": "sync_reset", + "returned": 0 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/sync-changes.07-cycle-off.json.snap b/tests/integration/routes/parity/__snapshots__/sync-changes.07-cycle-off.json.snap new file mode 100644 index 000000000..e26c893fc --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/sync-changes.07-cycle-off.json.snap @@ -0,0 +1,191 @@ +{ + "status": 200, + "body": { + "data": { + "serverNow": "", + "cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzkyMDUwNjAwMDAwLCJpIjoibS0wNiJ9LCJtb29kIjp7InUiOjE3OTIwNTE4MDAwMDAsImkiOiJtb29kLTAyIn0sImludGFrZXMiOnsidSI6MTc5MjA1MzYwMDAwMCwiaSI6ImluLTAzIn19fQ", + "hasMore": false, + "cursorExpired": false, + "changes": { + "measurements": { + "upserts": [ + { + "id": "m-01", + "externalId": null, + "type": "WEIGHT", + "value": 81.2, + "unit": "kg", + "measuredAt": "2026-10-14T06:30:00.000Z", + "source": "MANUAL", + "notes": "after the run", + "syncVersion": "", + "updatedAt": "" + }, + { + "id": "m-02", + "externalId": "w-pulse-1", + "type": "PULSE", + "value": 61, + "unit": "bpm", + "measuredAt": "2026-10-14T07:00:00.000Z", + "source": "WITHINGS", + "notes": null, + "syncVersion": "", + "updatedAt": "" + }, + { + "id": "m-04", + "externalId": "stats:HKQuantityTypeIdentifierHeartRate:2026-06-17T09", + "type": "PULSE", + "value": 64, + "unit": "bpm", + "measuredAt": "2026-06-17T09:30:00.000Z", + "source": "APPLE_HEALTH", + "notes": null, + "syncVersion": "", + "updatedAt": "" + }, + { + "id": "m-06", + "externalId": null, + "type": "BLOOD_PRESSURE_SYS", + "value": 128, + "unit": "mmHg", + "measuredAt": "2026-10-14T08:00:00.000Z", + "source": "MANUAL", + "notes": null, + "syncVersion": "", + "updatedAt": "" + } + ], + "tombstones": [ + { + "id": "m-03", + "externalId": "w-weight-2", + "syncVersion": "", + "deletedAt": "2026-10-15T11:30:00.000Z", + "updatedAt": "" + } + ] + }, + "mood": { + "upserts": [ + { + "id": "mood-01", + "date": "2026-10-14", + "mood": "GUT", + "score": 4, + "a1": 7, + "a2": 3, + "a3": null, + "a4": null, + "a5": null, + "context": { + "workStatus": "regular", + "workMinutes": 480, + "overtimeMinutes": null, + "workLoad": null, + "workSatisfaction": null, + "contactCircles": [], + "contactForm": null, + "contactExtent": null, + "contactQuality": null, + "contactSupport": null, + "leisureCategories": [], + "leisureMinutes": null, + "leisureJoy": null, + "leisureRecovery": null, + "eventType": null, + "eventValence": null, + "eventAt": null, + "note": null + }, + "tags": "[\"slept well\"]", + "note": "a calm morning", + "moodLoggedAt": "2026-10-14T07:00:00.000Z", + "source": "MANUAL", + "syncVersion": "", + "updatedAt": "" + } + ], + "tombstones": [ + { + "id": "mood-02", + "syncVersion": "", + "deletedAt": "2026-10-15T11:30:00.000Z", + "updatedAt": "" + } + ] + }, + "intakes": { + "upserts": [ + { + "id": "in-01", + "medicationId": "", + "scheduledFor": "2026-10-14T06:00:00.000Z", + "takenAt": "2026-10-14T06:10:00.000Z", + "skipped": false, + "source": "WEB", + "autoRecorded": false, + "syncVersion": "", + "updatedAt": "" + }, + { + "id": "in-02", + "medicationId": "", + "scheduledFor": "2026-10-13T06:00:00.000Z", + "takenAt": "2026-10-13T06:00:00.000Z", + "skipped": false, + "source": "WEB", + "autoRecorded": true, + "syncVersion": "", + "updatedAt": "" + } + ], + "tombstones": [ + { + "id": "in-03", + "syncVersion": "", + "deletedAt": "2026-10-15T11:30:00.000Z", + "updatedAt": "" + } + ] + }, + "cycleDays": { + "upserts": [], + "tombstones": [] + }, + "cycles": { + "upserts": [], + "tombstones": [] + } + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "sync.changes.pull" + }, + "meta": { + "measurement_upserts": 4, + "measurement_tombstones": 1, + "mood_upserts": 1, + "mood_tombstones": 1, + "intake_upserts": 2, + "intake_tombstones": 1, + "cycle_day_upserts": 0, + "cycle_day_tombstones": 0, + "cycle_upserts": 0, + "cycle_tombstones": 0, + "cycle_module_enabled": false, + "has_more": false, + "cursor_present": false + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/sync-changes.08-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/sync-changes.08-invalid.json.snap new file mode 100644 index 000000000..e36ea56fb --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/sync-changes.08-invalid.json.snap @@ -0,0 +1,29 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Invalid sync query", + "details": { + "issues": [ + { + "path": "limit", + "code": "too_small", + "message": "Too small: expected number to be >=1" + } + ] + }, + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.delete-01-synced.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-01-synced.json.snap new file mode 100644 index 000000000..0791cfb77 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-01-synced.json.snap @@ -0,0 +1,70 @@ +{ + "status": 409, + "body": { + "data": null, + "error": "Only a workout entered by hand can be deleted here", + "meta": { + "errorCode": "workout.delete.synced_source", + "requestId": "" + } + }, + "events": [ + { + "status": 409, + "action": { + "name": "workout.delete", + "entity_type": "workout", + "entity_id": "" + }, + "meta": { + "outcome": "synced_source", + "source": "APPLE_HEALTH" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "workouts": [ + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-run-0", + "sportType": "running" + }, + { + "id": "", + "source": "MANUAL", + "externalId": "manual:walk-1", + "sportType": "walking" + }, + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-ride-1", + "sportType": "cycling" + }, + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-run-1", + "sportType": "running" + }, + { + "id": "", + "source": "MANUAL", + "externalId": "manual:run-twin", + "sportType": "running" + } + ], + "records": [ + { + "metricSlot": "longest_distance_run", + "value": 3000, + "externalId": "manual:walk-1", + "source": "MANUAL" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.delete-02-missing.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-02-missing.json.snap new file mode 100644 index 000000000..d8364db37 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-02-missing.json.snap @@ -0,0 +1,21 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Workout not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.delete-03-other-account.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-03-other-account.json.snap new file mode 100644 index 000000000..47e99ce41 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-03-other-account.json.snap @@ -0,0 +1,63 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Workout not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "workouts": [ + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-run-0", + "sportType": "running" + }, + { + "id": "", + "source": "MANUAL", + "externalId": "manual:walk-1", + "sportType": "walking" + }, + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-ride-1", + "sportType": "cycling" + }, + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-run-1", + "sportType": "running" + }, + { + "id": "", + "source": "MANUAL", + "externalId": "manual:run-twin", + "sportType": "running" + } + ], + "records": [ + { + "metricSlot": "longest_distance_run", + "value": 3000, + "externalId": "manual:walk-1", + "source": "MANUAL" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.delete-04-manual.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-04-manual.json.snap new file mode 100644 index 000000000..1d47c0edf --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-04-manual.json.snap @@ -0,0 +1,65 @@ +{ + "status": 200, + "body": { + "data": { + "deleted": true + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "workout.delete", + "entity_type": "workout", + "entity_id": "" + }, + "meta": { + "outcome": "deleted", + "personalRecordsRemoved": 1 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueuePrDetection", + "args": [ + "", + { + "silent": true + } + ] + } + ], + "stored": { + "workouts": [ + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-run-0", + "sportType": "running" + }, + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-ride-1", + "sportType": "cycling" + }, + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-run-1", + "sportType": "running" + }, + { + "id": "", + "source": "MANUAL", + "externalId": "manual:run-twin", + "sportType": "running" + } + ], + "records": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-01-canonical.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-01-canonical.json.snap new file mode 100644 index 000000000..b4b299c13 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-01-canonical.json.snap @@ -0,0 +1,182 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "sportType": "running", + "startedAt": "2026-10-14T06:30:00.000Z", + "endedAt": "2026-10-14T06:46:00.000Z", + "dayKey": "2026-10-14", + "durationSec": 960, + "distanceM": 3200, + "activeEnergyKcal": 260, + "avgHr": 146, + "maxHr": 158, + "minHr": null, + "storedAvgHr": 146, + "storedMaxHr": 158, + "stepCount": null, + "elevationM": 6, + "pauseDurationSec": null, + "source": "APPLE_HEALTH", + "externalId": "hk-run-1", + "metadata": null, + "route": { + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 7.2162, + 51.4818, + 98 + ], + [ + 7.2262, + 51.4868, + 104 + ], + [ + 7.2383, + 51.4929, + 101 + ] + ] + }, + "sampleTimestamps": [ + { + "t": "2026-10-14T06:30:00.000Z", + "hr": 120 + }, + { + "t": "2026-10-14T06:38:00.000Z", + "hr": 141 + }, + { + "t": "2026-10-14T06:46:00.000Z", + "hr": 150 + } + ] + }, + "samples": { + "sampleCount": 4, + "samples": [ + { + "t": "2026-10-14T06:30:00.000Z", + "hr": 121 + }, + { + "t": "2026-10-14T06:35:00.000Z", + "hr": 142 + }, + { + "t": "2026-10-14T06:40:00.000Z", + "hr": 151 + }, + { + "t": "2026-10-14T06:45:00.000Z", + "hr": 147 + } + ] + }, + "hrSeries": { + "source": "workout_series", + "bucketSec": 5, + "points": [ + { + "tSec": 0, + "mean": 121, + "min": 121, + "max": 121 + }, + { + "tSec": 300, + "mean": 142, + "min": 142, + "max": 142 + }, + { + "tSec": 600, + "mean": 151, + "min": 151, + "max": 151 + }, + { + "tSec": 900, + "mean": 147, + "min": 147, + "max": 147 + } + ], + "envelope": false + }, + "zones": { + "model": "tanaka", + "hrMax": 176, + "zones": [ + { + "zone": 1, + "lowBpm": 88, + "highBpm": 106, + "seconds": 0 + }, + { + "zone": 2, + "lowBpm": 106, + "highBpm": 123, + "seconds": 5 + }, + { + "zone": 3, + "lowBpm": 123, + "highBpm": 141, + "seconds": 0 + }, + { + "zone": 4, + "lowBpm": 141, + "highBpm": 158, + "seconds": 15 + }, + { + "zone": 5, + "lowBpm": 158, + "highBpm": null, + "seconds": 0 + } + ] + }, + "splits": null, + "sportContext": { + "count": 1, + "avgDurationSec": 1800, + "avgDistanceM": 5000, + "avgAvgHr": 140 + }, + "aiInsight": null, + "ai": { + "available": false, + "reason": "no_provider", + "onDeviceAllowed": true + }, + "canonicalId": "", + "previousWorkoutId": "" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "workouts.detail.hr_series" + }, + "meta": { + "workoutId": "", + "source": "workout_series", + "points": 4 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-02-compact.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-02-compact.json.snap new file mode 100644 index 000000000..d1510e4c2 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-02-compact.json.snap @@ -0,0 +1,152 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "sportType": "running", + "startedAt": "2026-10-14T06:30:00.000Z", + "endedAt": "2026-10-14T06:46:00.000Z", + "dayKey": "2026-10-14", + "durationSec": 960, + "distanceM": 3200, + "activeEnergyKcal": 260, + "avgHr": 146, + "maxHr": 158, + "minHr": null, + "storedAvgHr": 146, + "storedMaxHr": 158, + "stepCount": null, + "elevationM": 6, + "pauseDurationSec": null, + "source": "APPLE_HEALTH", + "externalId": "hk-run-1", + "metadata": null, + "route": { + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 7.2162, + 51.4818, + 98 + ], + [ + 7.2262, + 51.4868, + 104 + ], + [ + 7.2383, + 51.4929, + 101 + ] + ] + }, + "sampleTimestamps": null + }, + "samples": { + "sampleCount": 4, + "samples": null + }, + "hrSeries": { + "source": "workout_series", + "bucketSec": 5, + "points": [ + { + "tSec": 0, + "mean": 121, + "min": 121, + "max": 121 + }, + { + "tSec": 300, + "mean": 142, + "min": 142, + "max": 142 + }, + { + "tSec": 600, + "mean": 151, + "min": 151, + "max": 151 + }, + { + "tSec": 900, + "mean": 147, + "min": 147, + "max": 147 + } + ], + "envelope": false + }, + "zones": { + "model": "tanaka", + "hrMax": 176, + "zones": [ + { + "zone": 1, + "lowBpm": 88, + "highBpm": 106, + "seconds": 0 + }, + { + "zone": 2, + "lowBpm": 106, + "highBpm": 123, + "seconds": 5 + }, + { + "zone": 3, + "lowBpm": 123, + "highBpm": 141, + "seconds": 0 + }, + { + "zone": 4, + "lowBpm": 141, + "highBpm": 158, + "seconds": 15 + }, + { + "zone": 5, + "lowBpm": 158, + "highBpm": null, + "seconds": 0 + } + ] + }, + "splits": null, + "sportContext": { + "count": 1, + "avgDurationSec": 1800, + "avgDistanceM": 5000, + "avgAvgHr": 140 + }, + "aiInsight": null, + "ai": { + "available": false, + "reason": "no_provider", + "onDeviceAllowed": true + }, + "canonicalId": "", + "previousWorkoutId": "" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "workouts.detail.hr_series" + }, + "meta": { + "workoutId": "", + "source": "workout_series", + "points": 4 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-03-hidden-twin.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-03-hidden-twin.json.snap new file mode 100644 index 000000000..f7e3fe114 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-03-hidden-twin.json.snap @@ -0,0 +1,62 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "sportType": "running", + "startedAt": "2026-10-14T06:30:20.000Z", + "endedAt": "2026-10-14T06:46:20.000Z", + "dayKey": "2026-10-14", + "durationSec": 960, + "distanceM": 3150, + "activeEnergyKcal": null, + "avgHr": 146, + "maxHr": 158, + "minHr": null, + "storedAvgHr": null, + "storedMaxHr": null, + "stepCount": null, + "elevationM": null, + "pauseDurationSec": null, + "source": "MANUAL", + "externalId": "manual:run-twin", + "metadata": null, + "route": null, + "samples": null, + "hrSeries": null, + "zones": null, + "splits": null, + "sportContext": { + "count": 1, + "avgDurationSec": 1800, + "avgDistanceM": 5000, + "avgAvgHr": 140 + }, + "aiInsight": null, + "ai": { + "available": false, + "reason": "no_provider", + "onDeviceAllowed": true + }, + "canonicalId": "", + "previousWorkoutId": "" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "workouts.detail.hr_series" + }, + "meta": { + "workoutId": "", + "source": "none", + "points": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-04-ride.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-04-ride.json.snap new file mode 100644 index 000000000..32fbc0f48 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-04-ride.json.snap @@ -0,0 +1,57 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "sportType": "cycling", + "startedAt": "2026-10-12T16:00:00.000Z", + "endedAt": "2026-10-12T17:00:00.000Z", + "dayKey": "2026-10-12", + "durationSec": 3600, + "distanceM": 24000, + "activeEnergyKcal": null, + "avgHr": null, + "maxHr": null, + "minHr": null, + "storedAvgHr": null, + "storedMaxHr": null, + "stepCount": null, + "elevationM": null, + "pauseDurationSec": null, + "source": "APPLE_HEALTH", + "externalId": "hk-ride-1", + "metadata": null, + "route": null, + "samples": null, + "hrSeries": null, + "zones": null, + "splits": null, + "sportContext": null, + "aiInsight": null, + "ai": { + "available": false, + "reason": "no_provider", + "onDeviceAllowed": true + }, + "canonicalId": "", + "previousWorkoutId": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "workouts.detail.hr_series" + }, + "meta": { + "workoutId": "", + "source": "none", + "points": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-05-other-account.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-05-other-account.json.snap new file mode 100644 index 000000000..6ece14ae3 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-05-other-account.json.snap @@ -0,0 +1,25 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Workout not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": { + "name": "workouts.detail" + }, + "meta": { + "workoutId": "" + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-06-missing.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-06-missing.json.snap new file mode 100644 index 000000000..550f1d9d3 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-06-missing.json.snap @@ -0,0 +1,25 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Workout not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": { + "name": "workouts.detail" + }, + "meta": { + "workoutId": "cmissingworkout000000000" + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-07-module-off.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-07-module-off.json.snap new file mode 100644 index 000000000..ad700627a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-07-module-off.json.snap @@ -0,0 +1,26 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "Module \"workouts\" is not enabled", + "meta": { + "errorCode": "module.disabled", + "module": "workouts", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": { + "name": "workouts.detail" + }, + "meta": { + "workoutId": "" + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/analytics.parity.test.ts b/tests/integration/routes/parity/analytics.parity.test.ts new file mode 100644 index 000000000..18e8b83e9 --- /dev/null +++ b/tests/integration/routes/parity/analytics.parity.test.ts @@ -0,0 +1,265 @@ +/** + * Parity for `GET /api/analytics` while the response builder moves out of + * the route into `src/lib/analytics/response/`. + * + * One account with a full record (see `record-fixture.ts`) walks through the + * cases the route tells apart, in order, each request seeing the cache the + * earlier ones filled: a `slice` outside the closed set, the summaries slice + * cold and warm, the default body cold and warm, the summaries slice after + * the glucose, sleep and recovery modules were switched off (gated after the + * cache read), and the default body in another language with a personal + * blood-pressure target (a fresh cache cell, so the builder runs against the + * clinical band a second time and with glucose off). An account with no + * profile and no data, and an anonymous call, close the walk. After each + * request the snapshot holds the response and its cache header, the wide + * event, the rollup refreshes the request handed off, and the stored health + * scores and correlation patterns. + * + * The clock is pinned (`Date` only) to noon of the day the test runs; see + * `NOW`. The record's rollups are built once + * before the walk; the fire-and-forget refresh the route starts on every + * request is recorded instead of run, so the wide event never depends on + * whether it finished first. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { GET } from "@/app/api/analytics/route"; +import { settleBackgroundTasks } from "@/lib/logging/background-tasks"; + +import { anonymous, callRoute, type RouteAuth } from "../../../kit/call-route"; +import { makeSession, makeUser } from "../../../kit/factories"; +import { + effectRecorder, + eventFacts, + expectParity, + relativeToAnchor, + stable, + startOfUtcToday, + wideEventRecorder, +} from "../../../kit/parity"; +import { getPrismaClient, truncateAllTables } from "../../setup"; +import { parityId, seedParityRecord } from "./record-fixture"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); + +vi.mock("@/lib/rollups/measurement-rollups", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + ensureUserRollupsFresh: async (...args: unknown[]) => { + effectRecorder.record("ensureUserRollupsFresh", args); + }, + }; +}); + +const prisma = getPrismaClient(); +/** + * Noon of the current UTC day. The summaries read their 90-day windows in SQL + * against the database clock, which a pinned `Date` cannot move, so the clock + * is pinned to today and the snapshot records positions relative to today's + * start (`relativeToAnchor`). The record is under sixty days old, so no + * reading sits on a SQL window's edge whatever the time of day. + */ +const ANCHOR = startOfUtcToday(); +const NOW = new Date(ANCHOR.getTime() + 12 * 60 * 60 * 1000); +const TZ = "UTC"; +const OWNER_ID = parityId("analytics", 1); +const EMPTY_ID = parityId("analytics", 2); + +let owner: RouteAuth; +let empty: RouteAuth; + +async function stored(userId: string) { + const [healthScores, patterns] = await Promise.all([ + prisma.healthScoreRecord.findMany({ + where: { userId }, + orderBy: { dayKey: "asc" }, + select: { + dayKey: true, + timezone: true, + composite: true, + band: true, + scoreVersion: true, + composition: true, + pillarScores: true, + inputFingerprint: true, + computedAt: true, + }, + }), + prisma.correlationPattern.findMany({ + where: { userId }, + orderBy: { canonicalKey: "asc" }, + select: { + canonicalKey: true, + family: true, + factorKey: true, + outcomeKey: true, + lagDays: true, + sampleSize: true, + effectSize: true, + pValue: true, + qValue: true, + evidenceHash: true, + isCurrent: true, + lastComputedAt: true, + }, + }), + ]); + return { healthScores, patterns }; +} + +async function step( + name: string, + options: { + auth?: RouteAuth; + query?: Record; + userId?: string; + } = {}, +): Promise { + wideEventRecorder.take(); + effectRecorder.take(); + const res = await callRoute(GET, { + method: "GET", + route: "/api/analytics", + query: options.query, + auth: options.auth ?? owner, + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + // Work the request left running (a rollup fill behind a read) finishes + // before the next request, so each case starts from the same store. + await settleBackgroundTasks(); + await expectParity( + `analytics.${name}`, + stable( + relativeToAnchor( + { + status: res.status, + cacheControl: res.headers.get("cache-control"), + body: res.body, + events: events.map(eventFacts).map(withoutRollupCoverage), + effects: effectRecorder.take(), + stored: await stored(options.userId ?? OWNER_ID), + }, + ANCHOR, + ), + { since: NOW }, + ), + ); +} + +/** + * The blood-pressure fast path reports how many rollup rows it read. That + * count follows the rollup tier's coverage at the moment of the request, + * which background refills outside this route decide, so it differs between + * two runs of the same code while the answer it fed does not. The answer + * stays pinned in `body`; only the three diagnostic counts are dropped. + */ +function withoutRollupCoverage(facts: unknown): unknown { + const meta = (facts as { meta?: { analytics?: { bp_in_target?: object } } }) + .meta; + const bp = meta?.analytics?.bp_in_target as + Record | undefined; + if (bp) { + delete bp.row_count; + delete bp.sys_rows; + delete bp.dia_rows; + } + return facts; +} + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const ownerRow = await makeUser({ + id: OWNER_ID, + timezone: TZ, + locale: "en", + heightCm: 178, + dateOfBirth: new Date("1975-04-02T00:00:00.000Z"), + gender: "male", + }); + await seedParityRecord({ userId: OWNER_ID, now: NOW, tz: TZ, tag: "an" }); + const rollups = await vi.importActual< + typeof import("@/lib/rollups/measurement-rollups") + >("@/lib/rollups/measurement-rollups"); + await rollups.ensureUserRollupsFresh(OWNER_ID); + await settleBackgroundTasks(); + owner = await makeSession(ownerRow); + const emptyRow = await makeUser({ id: EMPTY_ID, timezone: "UTC" }); + empty = await makeSession(emptyRow); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("GET /api/analytics parity", () => { + it("01 a slice outside the closed set", async () => { + await step("01-invalid-slice", { query: { slice: "summary" } }); + }); + + it("02 the summaries slice, cold", async () => { + await step("02-summaries-cold", { query: { slice: "summaries" } }); + }); + + it("03 the summaries slice, warm", async () => { + await step("03-summaries-warm", { query: { slice: "summaries" } }); + }); + + it("04 the default body, cold", async () => { + await step("04-default-cold"); + }); + + it("05 the default body, warm", async () => { + await step("05-default-warm"); + }); + + it("06 the summaries slice with modules off", async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { + modulePreferencesJson: { + glucose: false, + sleep: false, + recovery: false, + }, + }, + }); + await step("06-summaries-modules-off", { query: { slice: "summaries" } }); + }); + + it("07 the default body in another language with a personal target", async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { + locale: "de", + thresholdsJson: { + BLOOD_PRESSURE_SYS: { min: 100, max: 125 }, + BLOOD_PRESSURE_DIA: { min: 60, max: 80 }, + }, + }, + }); + await step("07-default-de-personal-target"); + }); + + it("08 an account with no profile and no data", async () => { + await step("08-empty-account", { auth: empty, userId: EMPTY_ID }); + }); + + it("09 an anonymous call", async () => { + await step("09-anonymous", { auth: anonymous }); + }); +}); diff --git a/tests/integration/routes/parity/dashboard-summary.parity.test.ts b/tests/integration/routes/parity/dashboard-summary.parity.test.ts new file mode 100644 index 000000000..e70743b6b --- /dev/null +++ b/tests/integration/routes/parity/dashboard-summary.parity.test.ts @@ -0,0 +1,201 @@ +/** + * Parity for `GET /api/dashboard/summary` while its builder moves into + * `src/lib/dashboard/`. + * + * One record carries every block the summary shapes (`record-fixture.ts`): + * the walk reads it cold, warm, with modules off (gated after the cache), + * in another language (served from the cache that still holds the first + * greeting), for a delegate who may manage the record and one who may only + * read it, without credentials, and for an account with no data. The + * fire-and-forget rollup refresh is recorded instead of run. + * + * The clock is pinned to noon of the current UTC day and the snapshot is + * written relative to the start of that day (`relativeToAnchor`), because + * the per-type reads cut their windows in SQL against the database clock. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { GET } from "@/app/api/dashboard/summary/route"; +import { settleBackgroundTasks } from "@/lib/logging/background-tasks"; + +import { anonymous, callRoute, type RouteAuth } from "../../../kit/call-route"; +import { makeGrant, makeSession, makeUser } from "../../../kit/factories"; +import { + effectRecorder, + eventFacts, + expectParity, + relativeToAnchor, + stable, + startOfUtcToday, + wideEventRecorder, +} from "../../../kit/parity"; +import { + getPrismaClient, + switchSessionTo, + truncateAllTables, +} from "../../setup"; +import { parityId, seedParityRecord } from "./record-fixture"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); +vi.mock("@/lib/rollups/measurement-rollups", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + ensureUserRollupsFresh: async (...args: unknown[]) => { + effectRecorder.record("ensureUserRollupsFresh", args); + }, + }; +}); + +const prisma = getPrismaClient(); +const ANCHOR = startOfUtcToday(); +const NOW = new Date(ANCHOR.getTime() + 12 * 60 * 60 * 1000); +const TZ = "UTC"; +const OWNER_ID = parityId("dashsum", 1); +const EMPTY_ID = parityId("dashsum", 2); + +let owner: RouteAuth; +let empty: RouteAuth; +let manager: RouteAuth & { sessionId: string }; +let reader: RouteAuth & { sessionId: string }; + +async function step( + name: string, + auth: RouteAuth = owner, + contract?: { skip: string }, +): Promise { + wideEventRecorder.take(); + effectRecorder.take(); + const res = await callRoute(GET, { + method: "GET", + route: "/api/dashboard/summary", + auth, + ...(contract ? { contract } : {}), + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + await settleBackgroundTasks(); + await expectParity( + `dashboard-summary.${name}`, + stable( + relativeToAnchor( + { + status: res.status, + body: res.body, + events: events.map(eventFacts).map(withSortedMeta), + effects: effectRecorder.take(), + }, + ANCHOR, + ), + { since: NOW }, + ), + ); +} + +/** + * The builder's sub-reads run concurrently and each annotates its own timing + * when it settles, so the meta keys arrive in a different order on each run. + * The values are the same; the keys are sorted. + */ +function withSortedMeta(facts: unknown): unknown { + const f = facts as { meta?: Record }; + if (!f.meta) return facts; + return { + ...f, + meta: Object.fromEntries( + Object.entries(f.meta).sort(([a], [b]) => a.localeCompare(b)), + ), + }; +} + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const ownerRow = await makeUser({ + id: OWNER_ID, + timezone: TZ, + locale: "en", + displayName: "Sam", + heightCm: 178, + dateOfBirth: new Date("1975-04-02T00:00:00.000Z"), + gender: "male", + }); + await seedParityRecord({ userId: OWNER_ID, now: NOW, tz: TZ, tag: "ds" }); + const rollups = await vi.importActual< + typeof import("@/lib/rollups/measurement-rollups") + >("@/lib/rollups/measurement-rollups"); + await rollups.ensureUserRollupsFresh(OWNER_ID); + await settleBackgroundTasks(); + owner = await makeSession(ownerRow); + const emptyRow = await makeUser({ + id: EMPTY_ID, + username: "empty-account", + timezone: "UTC", + }); + empty = await makeSession(emptyRow); + const managerRow = await makeUser({ timezone: "UTC", locale: "de" }); + await makeGrant(ownerRow, managerRow, { access: "MANAGE" }); + manager = await makeSession(managerRow); + const readerRow = await makeUser({ timezone: "UTC" }); + await makeGrant(ownerRow, readerRow); + reader = await makeSession(readerRow); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("GET /api/dashboard/summary parity", () => { + it("01 cold, then warm", async () => { + await step("01-cold"); + await step("02-warm"); + }); + + it("03 modules off, gated after the cache", async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { + modulePreferencesJson: { glucose: false, sleep: false, mood: false }, + }, + }); + await step("03-modules-off"); + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { modulePreferencesJson: {} }, + }); + }); + + it("04 another language, served from the cache", async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { locale: "de" }, + }); + await step("04-de-cached"); + }); + + it("05 a delegate who may manage, and one who may only read", async () => { + await switchSessionTo(manager.sessionId, OWNER_ID); + await step("05-manager", manager); + await switchSessionTo(manager.sessionId, null); + await switchSessionTo(reader.sessionId, OWNER_ID); + await step("06-reader", reader); + await switchSessionTo(reader.sessionId, null); + }); + + it("07 no credentials, and an account with no data", async () => { + await step("07-anonymous", anonymous); + await step("08-empty-account", empty); + }); +}); diff --git a/tests/integration/routes/parity/documents-inbound.parity.test.ts b/tests/integration/routes/parity/documents-inbound.parity.test.ts new file mode 100644 index 000000000..81ef63c24 --- /dev/null +++ b/tests/integration/routes/parity/documents-inbound.parity.test.ts @@ -0,0 +1,343 @@ +/** + * Parity for `POST` and `GET /api/documents/inbound` while the upload + * request handling and the list read move into `src/lib/documents/`. + * + * The upload walk: a stored PDF with metadata and an episode link, the same + * bytes again, each refusal the route tells apart (no file, invalid + * metadata, an empty file, an unsupported type, an unknown episode, half a + * source key, a query and a form key that disagree), a narrow + * `documents:write` token storing under a source key with AI reading held + * back, its re-send by query key, its re-send after the person deleted the + * document, and the module switched off. The list walk: the default page, + * a search, the filters, a sorted page and the page after it, a refused + * query, and a delegate reading the record. After each request the snapshot + * holds the response, the wide event, the recorded queue tails and, for the + * uploads, the stored documents. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { GET, POST } from "@/app/api/documents/inbound/route"; +import { DOCUMENTS_WRITE_SCOPE } from "@/lib/documents/scopes"; + +import { callRoute, type RouteAuth } from "../../../kit/call-route"; +import { + makeGrant, + makeSession, + makeToken, + makeUser, +} from "../../../kit/factories"; +import { + effectRecorder, + eventFacts, + expectParity, + stable, + wideEventRecorder, +} from "../../../kit/parity"; +import { + getPrismaClient, + switchSessionTo, + truncateAllTables, +} from "../../setup"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); +vi.mock("@/lib/jobs/document-index", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + enqueueDocumentIndex: async (...args: unknown[]) => { + effectRecorder.record("enqueueDocumentIndex", args); + }, + }; +}); +vi.mock("@/lib/jobs/document-thumbnail", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + enqueueDocumentThumbnail: async (...args: unknown[]) => { + effectRecorder.record("enqueueDocumentThumbnail", args); + }, + }; +}); +vi.mock("@/lib/jobs/document-summary", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + enqueueDocumentSummary: async (...args: unknown[]) => { + effectRecorder.record("enqueueDocumentSummary", args); + }, + }; +}); + +const prisma = getPrismaClient(); +const NOW = new Date("2026-10-15T12:00:00.000Z"); +const PDF = (tag: string) => + Buffer.from(`%PDF-1.7\n% ${tag}\n1 0 obj\n<<>>\nendobj\n%%EOF\n`); + +let userId: string; +let owner: RouteAuth; +let writer: RouteAuth; +let delegate: RouteAuth & { sessionId: string }; +let episodeId: string; + +async function stored() { + return prisma.inboundDocument.findMany({ + where: { userId }, + orderBy: [{ createdAt: "asc" }, { filename: "asc" }], + select: { + id: true, + kind: true, + title: true, + filename: true, + mimeType: true, + byteSize: true, + status: true, + documentDate: true, + sourceSystem: true, + sourceId: true, + deletedAt: true, + conditionLinks: { select: { episodeId: true } }, + }, + }); +} + +function form( + file: Buffer | null, + name: string, + fields: Record = {}, +): FormData { + const data = new FormData(); + if (file) data.set("file", new File([new Uint8Array(file)], name)); + for (const [key, value] of Object.entries(fields)) { + for (const v of Array.isArray(value) ? value : [value]) { + data.append(key, v); + } + } + return data; +} + +async function upload( + name: string, + body: FormData, + options: { auth?: RouteAuth; query?: Record } = {}, +): Promise<{ id: string | null }> { + wideEventRecorder.take(); + effectRecorder.take(); + const res = await callRoute<{ id: string | null }>(POST, { + method: "POST", + route: "/api/documents/inbound", + body, + query: options.query, + auth: options.auth ?? owner, + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + await expectParity( + `documents-inbound.${name}`, + stable( + { + status: res.status, + body: res.body, + events: events.map(eventFacts), + effects: effectRecorder.take(), + stored: await stored(), + }, + { since: NOW }, + ), + ); + return { id: res.data?.id ?? null }; +} + +async function list( + name: string, + query: Record, + auth: RouteAuth = owner, +): Promise<{ nextCursor: string | null }> { + wideEventRecorder.take(); + const res = await callRoute<{ nextCursor: string | null }>(GET, { + method: "GET", + route: "/api/documents/inbound", + query, + auth, + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + await expectParity( + `documents-inbound.${name}`, + stable( + { status: res.status, body: res.body, events: events.map(eventFacts) }, + { since: NOW }, + ), + ); + return { nextCursor: res.data?.nextCursor ?? null }; +} + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const user = await makeUser({ + timezone: "Europe/Berlin", + modulePreferencesJson: { inboundDocuments: true }, + }); + userId = user.id; + owner = await makeSession(user); + writer = await makeToken(user, { permissions: [DOCUMENTS_WRITE_SCOPE] }); + const episode = await prisma.illnessEpisode.create({ + data: { + userId, + label: "Knee", + type: "OTHER", + onsetAt: new Date("2026-09-01T00:00:00.000Z"), + }, + }); + episodeId = episode.id; + const reader = await makeUser({ timezone: "UTC" }); + await makeGrant(user, reader); + delegate = await makeSession(reader); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("POST /api/documents/inbound parity", () => { + it("a stored upload and the same bytes again", async () => { + const body = () => + form(PDF("discharge"), "discharge.pdf", { + title: "Discharge letter", + kind: "DISCHARGE_LETTER", + documentDate: "2026-10-02", + episodeIds: [episodeId], + }); + await upload("post-01-stored", body()); + await upload("post-02-duplicate", body()); + await upload( + "post-03-second", + form(PDF("labs"), "labs.pdf", { + title: "Blood test", + kind: "LAB_RESULT", + documentDate: "2025-03-04", + }), + ); + }); + + it("the refusals", async () => { + await upload("post-04-no-file", form(null, "x", { title: "Nothing" })); + await upload( + "post-05-invalid-metadata", + form(PDF("bad-kind"), "bad.pdf", { kind: "SHOPPING_LIST" }), + ); + await upload("post-06-empty", form(Buffer.alloc(0), "empty.pdf")); + await upload( + "post-07-unsupported", + form( + Buffer.from("hi\n"), + "page.html", + ), + ); + await upload( + "post-08-unknown-episode", + form(PDF("orphan"), "orphan.pdf", { + episodeIds: ["cmissingepisode000000000"], + }), + ); + await upload("post-09-half-key", form(PDF("half"), "half.pdf"), { + query: { sourceSystem: "PAPERLESS" }, + }); + await upload( + "post-10-keys-differ", + form(PDF("differ"), "differ.pdf", { + sourceSystem: "PAPERLESS", + sourceId: "77", + }), + { query: { sourceSystem: "PAPERLESS", sourceId: "78" } }, + ); + }); + + it("a narrow token storing under a source key", async () => { + const stored = await upload( + "post-11-token-stored", + form(PDF("imported"), "imported.pdf", { + sourceSystem: "PAPERLESS", + sourceId: "41", + aiRead: "defer", + }), + { auth: writer }, + ); + await upload("post-12-token-query-key", form(PDF("imported"), "x.pdf"), { + auth: writer, + query: { sourceSystem: "PAPERLESS", sourceId: "41" }, + }); + await prisma.inboundDocument.update({ + where: { id: stored.id! }, + data: { deletedAt: new Date("2026-10-14T09:00:00.000Z") }, + }); + await upload( + "post-13-token-deleted", + form(PDF("imported"), "imported.pdf", { + sourceSystem: "PAPERLESS", + sourceId: "41", + }), + { auth: writer }, + ); + }); + + it("the module switched off", async () => { + await prisma.user.update({ + where: { id: userId }, + data: { modulePreferencesJson: { inboundDocuments: false } }, + }); + await upload("post-14-module-off", form(PDF("dark"), "dark.pdf")); + await prisma.user.update({ + where: { id: userId }, + data: { modulePreferencesJson: { inboundDocuments: true } }, + }); + }); +}); + +describe("GET /api/documents/inbound parity", () => { + it("the default page, a search and the filters", async () => { + await list("get-01-default", {}); + await list("get-02-search", { q: "blood" }); + await list("get-03-kind", { kind: "DISCHARGE_LETTER,LAB_RESULT" }); + await list("get-04-episode", { episodeId }); + await list("get-05-year", { year: 2025 }); + await list("get-06-range", { from: "2026-10-01", to: "2026-10-02" }); + }); + + it("a sorted page and the page after it", async () => { + const first = await list("get-07-sorted", { + sort: "title", + order: "asc", + limit: 1, + }); + await list("get-08-next-page", { + sort: "title", + order: "asc", + limit: 1, + cursor: first.nextCursor!, + }); + }); + + it("a refused query, and a delegate reading the record", async () => { + await list("get-09-invalid", { sort: "size" }); + await switchSessionTo(delegate.sessionId, userId); + await list("get-10-delegated", {}, delegate); + await switchSessionTo(delegate.sessionId, null); + }); +}); diff --git a/tests/integration/routes/parity/import-csv.parity.test.ts b/tests/integration/routes/parity/import-csv.parity.test.ts new file mode 100644 index 000000000..60ee5ba50 --- /dev/null +++ b/tests/integration/routes/parity/import-csv.parity.test.ts @@ -0,0 +1,224 @@ +/** + * Parity for `POST /api/import/csv` while its batched writer moves into + * `src/lib/import/`. + * + * A dry run of a mixed file (rows with and without an external id, a key + * repeated inside the file, a natural-key twin, a row the parser refuses), + * the same file for real, the same file again, a changed file that moves a + * reading to another day and re-imports a deleted row, and the refusals (an + * unknown parameter, a file without a usable header). After each request + * the snapshot holds the response, the wide event, the recorded arrival and + * refresh tails, and the stored measurements, rollups and audit rows. The + * shared import bucket is emptied before each request so the walk never + * meets its own rate limit. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { POST } from "@/app/api/import/csv/route"; + +import { callRoute, type RouteAuth } from "../../../kit/call-route"; +import { makeSession, makeUser } from "../../../kit/factories"; +import { + effectRecorder, + eventFacts, + expectParity, + stable, + wideEventRecorder, +} from "../../../kit/parity"; +import { getPrismaClient, truncateAllTables } from "../../setup"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); +vi.mock("@/lib/arrivals/emit-shared", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + emitDataArrival: async (...args: unknown[]) => { + effectRecorder.record("emitDataArrival", args); + }, + }; +}); +vi.mock("@/lib/daily/morning-refresh-trigger", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/daily/morning-refresh-trigger") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + maybeEnqueueMorningRefresh: async (...args: unknown[]) => { + effectRecorder.record("maybeEnqueueMorningRefresh", args); + }, + }; +}); +vi.mock("@/lib/jobs/pr-detection", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + enqueuePrDetection: async (...args: unknown[]) => { + effectRecorder.record("enqueuePrDetection", args); + }, + }; +}); + +vi.mock("@/lib/insights/status-invalidation", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + // Detached from the request but inside its context: run for real, its + // annotate can land on the request's wide event or miss it, depending on + // timing. Recorded instead, like the other tails. + invalidateStatusInsightsForTypes: async (...args: unknown[]) => { + effectRecorder.record("invalidateStatusInsightsForTypes", args); + }, + }; +}); + +const prisma = getPrismaClient(); +const NOW = new Date("2026-10-15T12:00:00.000Z"); +const HEADER = "type,value,unit,measuredAt,glucoseContext,notes,externalId"; + +let userId: string; +let owner: RouteAuth; + +async function stored() { + const [measurements, rollups, audit] = await Promise.all([ + prisma.measurement.findMany({ + where: { userId }, + orderBy: [{ type: "asc" }, { measuredAt: "asc" }, { value: "asc" }], + select: { + type: true, + value: true, + unit: true, + source: true, + externalId: true, + measuredAt: true, + notes: true, + notesEncrypted: true, + glucoseContext: true, + deletedAt: true, + }, + }), + prisma.measurementRollup.findMany({ + where: { userId, granularity: "DAY" }, + orderBy: [{ type: "asc" }, { bucketStart: "asc" }], + select: { type: true, bucketStart: true, count: true, mean: true }, + }), + prisma.auditLog.findMany({ + where: { userId }, + orderBy: { createdAt: "asc" }, + select: { action: true, details: true }, + }), + ]); + return { measurements, rollups, audit }; +} + +async function step( + name: string, + lines: string[], + query: Record = {}, +): Promise { + await prisma.rateLimit.deleteMany({ where: { key: `import:${userId}` } }); + wideEventRecorder.take(); + effectRecorder.take(); + const res = await callRoute(POST, { + method: "POST", + route: "/api/import/csv", + query, + text: lines.join("\n"), + headers: { "content-type": "text/csv" }, + auth: owner, + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + await expectParity( + `import-csv.${name}`, + stable( + { + status: res.status, + body: res.body, + events: events.map(eventFacts), + effects: effectRecorder.take(), + stored: await stored(), + }, + { since: NOW }, + ), + ); +} + +const FILE = [ + HEADER, + "WEIGHT,81.4,kg,2026-10-10T06:30:00.000Z,,after the run,exp-w-1", + "WEIGHT,81.9,kg,2026-10-11T06:30:00.000Z,,,exp-w-2", + // The same key again later in the file: the last value wins. + "WEIGHT,82.0,kg,2026-10-11T06:30:00.000Z,,,exp-w-2", + "BLOOD_GLUCOSE,104,mg/dL,2026-10-11T08:00:00.000Z,FASTING,,", + // A natural-key twin of the line above. + "BLOOD_GLUCOSE,104,mg/dL,2026-10-11T08:00:00.000Z,FASTING,,", + "SLEEP_DURATION,412,minutes,2026-10-12T06:00:00.000Z,,,exp-s-1", + "PULSE,64,bpm,2026-10-12T07:00:00.000Z,,,", + // Refused by the parser. + "WEIGHT,not-a-number,kg,2026-10-12T06:30:00.000Z,,,", +]; + +const CHANGED = [ + HEADER, + // Unchanged. + "WEIGHT,81.4,kg,2026-10-10T06:30:00.000Z,,after the run,exp-w-1", + // Moved to another day. + "WEIGHT,82.0,kg,2026-10-09T06:30:00.000Z,,,exp-w-2", + // Deleted since the last import; re-importing brings it back. + "SLEEP_DURATION,430,minutes,2026-10-12T06:00:00.000Z,,,exp-s-1", + "PULSE,66,bpm,2026-10-13T07:00:00.000Z,,,", +]; + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const user = await makeUser({ timezone: "Europe/Berlin" }); + userId = user.id; + owner = await makeSession(user); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("POST /api/import/csv parity", () => { + it("01 a dry run", async () => { + await step("01-dry-run", FILE, { dryRun: "1" }); + }); + + it("02 the file for real, then again", async () => { + await step("02-first-import", FILE); + await step("03-same-file", FILE); + }); + + it("04 a changed file over a deleted row", async () => { + await prisma.measurement.updateMany({ + where: { userId, externalId: "exp-s-1" }, + data: { deletedAt: new Date("2026-10-13T08:00:00.000Z") }, + }); + await step("04-changed-file", CHANGED); + }); + + it("05 the refusals", async () => { + await step("05-unknown-parameter", FILE, { dryrun: "1" }); + await step("06-no-header", ["just,some,words"]); + }); +}); diff --git a/tests/integration/routes/parity/import-json.parity.test.ts b/tests/integration/routes/parity/import-json.parity.test.ts new file mode 100644 index 000000000..265de2378 --- /dev/null +++ b/tests/integration/routes/parity/import-json.parity.test.ts @@ -0,0 +1,297 @@ +/** + * Parity for `POST /api/import` while its writers move into + * `src/lib/import/json/`. + * + * A first file of mixed rows (measurements with and without an external id, + * with a note and a glucose context, an unstable id; mood entries with and + * without a logged instant, a twin pair that has to land on two distinct + * fallback instants, an external id, an unstable id), the same file again, + * and the whole-file refusals. After each request the snapshot holds the + * response, the wide event, the recorded arrival and refresh tails, and the + * stored measurements, mood entries, rollups and audit rows. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { POST } from "@/app/api/import/route"; + +import { callRoute, type RouteAuth } from "../../../kit/call-route"; +import { makeSession, makeUser } from "../../../kit/factories"; +import { + effectRecorder, + eventFacts, + expectParity, + stable, + wideEventRecorder, +} from "../../../kit/parity"; +import { getPrismaClient, truncateAllTables } from "../../setup"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); +vi.mock("@/lib/arrivals/emit-shared", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + emitDataArrival: async (...args: unknown[]) => { + effectRecorder.record("emitDataArrival", args); + }, + }; +}); +vi.mock("@/lib/daily/morning-refresh-trigger", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/daily/morning-refresh-trigger") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + maybeEnqueueMorningRefresh: async (...args: unknown[]) => { + effectRecorder.record("maybeEnqueueMorningRefresh", args); + }, + }; +}); +vi.mock("@/lib/jobs/pr-detection", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + enqueuePrDetection: async (...args: unknown[]) => { + effectRecorder.record("enqueuePrDetection", args); + }, + }; +}); + +vi.mock("@/lib/insights/status-invalidation", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + // Detached from the request but inside its context: run for real, its + // annotate can land on the request's wide event or miss it, depending on + // timing. Recorded instead, like the other tails. + invalidateStatusInsightsForTypes: async (...args: unknown[]) => { + effectRecorder.record("invalidateStatusInsightsForTypes", args); + }, + }; +}); + +const prisma = getPrismaClient(); +const NOW = new Date("2026-10-15T12:00:00.000Z"); + +let userId: string; +let owner: RouteAuth; + +async function stored() { + const [measurements, mood, measurementRollups, moodRollups, audit] = + await Promise.all([ + prisma.measurement.findMany({ + where: { userId }, + orderBy: [{ measuredAt: "asc" }, { type: "asc" }, { value: "asc" }], + select: { + type: true, + value: true, + unit: true, + source: true, + externalId: true, + measuredAt: true, + notes: true, + notesEncrypted: true, + glucoseContext: true, + }, + }), + prisma.moodEntry.findMany({ + where: { userId }, + orderBy: [{ moodLoggedAt: "asc" }, { mood: "asc" }], + select: { + date: true, + mood: true, + score: true, + moodA1: true, + tags: true, + source: true, + externalId: true, + moodLoggedAt: true, + }, + }), + prisma.measurementRollup.findMany({ + where: { userId }, + orderBy: [ + { type: "asc" }, + { granularity: "asc" }, + { bucketStart: "asc" }, + ], + select: { + type: true, + granularity: true, + bucketStart: true, + source: true, + count: true, + mean: true, + }, + }), + prisma.moodEntryRollup.findMany({ + where: { userId }, + orderBy: [{ granularity: "asc" }, { bucketStart: "asc" }], + select: { + granularity: true, + bucketStart: true, + count: true, + mean: true, + }, + }), + prisma.auditLog.findMany({ + where: { userId }, + orderBy: { createdAt: "asc" }, + select: { action: true, details: true }, + }), + ]); + return { measurements, mood, measurementRollups, moodRollups, audit }; +} + +async function step(name: string, body: unknown): Promise { + wideEventRecorder.take(); + effectRecorder.take(); + const res = await callRoute(POST, { + method: "POST", + route: "/api/import", + body, + auth: owner, + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + await expectParity( + `import-json.${name}`, + stable( + { + status: res.status, + body: res.body, + events: events.map(eventFacts), + effects: effectRecorder.take(), + stored: await stored(), + }, + { since: NOW }, + ), + ); +} + +const FILE = { + measurements: [ + { + type: "WEIGHT", + value: 81.4, + unit: "kg", + measuredAt: "2026-10-10T06:30:00.000Z", + notes: "after the run", + externalId: "exp-w-1", + }, + { + type: "WEIGHT", + value: 81.9, + unit: "kg", + measuredAt: "2026-10-11T06:30:00.000Z", + }, + { + type: "BLOOD_GLUCOSE", + value: 104, + unit: "mg/dL", + measuredAt: "2026-10-11T08:00:00.000Z", + glucoseContext: "FASTING", + externalId: "exp-g-1", + }, + { + type: "SLEEP_DURATION", + value: 412, + unit: "min", + measuredAt: "2026-10-12T06:00:00.000Z", + externalId: "exp-s-1", + }, + { + type: "PULSE", + value: 64, + unit: "bpm", + measuredAt: "2026-10-12T07:00:00.000Z", + externalId: "", + }, + ], + moodEntries: [ + { + date: "2026-10-10", + mood: "GUT", + score: 4, + tags: "slept well", + loggedAt: "2026-10-10T07:00:00.000Z", + externalId: "daylio-1", + }, + { date: "2026-10-11", mood: "OKAY", score: 3 }, + { date: "2026-10-11", mood: "OKAY", score: 3 }, + { + date: "2026-10-12", + mood: "SCHLECHT", + score: 2, + loggedAt: "2026-10-12T20:00:00.000Z", + }, + { + date: "2026-10-13", + mood: "GUT", + score: 5, + externalId: "", + }, + ], +}; + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const user = await makeUser({ timezone: "Europe/Berlin" }); + userId = user.id; + owner = await makeSession(user); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("POST /api/import parity", () => { + it("01 a first file", async () => { + await step("01-first-file", FILE); + }); + + it("02 the same file again", async () => { + await step("02-same-file", FILE); + }); + + it("03 the whole-file refusals", async () => { + await step("03-out-of-range", { + measurements: [ + { + type: "WEIGHT", + value: 4_000, + unit: "kg", + measuredAt: "2026-10-10T06:30:00.000Z", + }, + ], + }); + await step("04-forward-dated", { + moodEntries: [ + { + date: "2026-10-20", + mood: "GUT", + score: 4, + loggedAt: "2026-10-20T07:00:00.000Z", + }, + ], + }); + }); +}); diff --git a/tests/integration/routes/parity/insights-comprehensive.parity.test.ts b/tests/integration/routes/parity/insights-comprehensive.parity.test.ts new file mode 100644 index 000000000..1518d3c8b --- /dev/null +++ b/tests/integration/routes/parity/insights-comprehensive.parity.test.ts @@ -0,0 +1,471 @@ +/** + * Parity for `GET /api/insights/comprehensive` while the overview builder + * moves out of the route into `src/lib/insights/`. + * + * One account with a full record (see `record-fixture.ts`) walks through the + * cases the route tells apart, in order, each request seeing the cache the + * earlier ones filled: an anonymous call, the overview cold, warm, and stale + * after a measurement write (served while one rebuild runs behind it), the + * `ai` block after the AI analysis opt-out and after the operator's master + * switch (both off the cached body), a delegate with MANAGE access reading + * the record cold, one with READ access refused, and the shared analytics + * read ceiling. A second account in a zone west of UTC carries the sparse + * shapes: mood entries with no rollup tier behind them, pulse without a + * resting series, a medication with no schedule, an as-needed one, and no + * height. An account with no data closes the walk. + * + * The clock is pinned (`Date` only) to noon of the day the test runs. The + * aggregator cuts its 7-, 30- and 60-day windows in SQL against the + * database clock, which the pin cannot reach, so the record's measurements + * on exactly those days back are removed: a reading there would fall in or + * out of a window with the hour the test runs. Every instant and day key in + * the snapshot is written relative to the start of the day. + * + * The rollup refreshes the route starts and does not await are recorded + * instead of run; the record's rollups are built once before the walk. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { GET } from "@/app/api/insights/comprehensive/route"; +import { __resetAllCachesForTests } from "@/lib/cache/server-cache"; +import { invalidateUserMeasurements } from "@/lib/cache/invalidate"; +import { settleBackgroundTasks } from "@/lib/logging/background-tasks"; + +import { anonymous, callRoute, type RouteAuth } from "../../../kit/call-route"; +import { + makeGrant, + makeSession, + makeUser, + type SessionFixture, +} from "../../../kit/factories"; +import { + effectRecorder, + eventFacts, + expectParity, + relativeToAnchor, + stable, + startOfUtcToday, + wideEventRecorder, +} from "../../../kit/parity"; +import { + getPrismaClient, + switchSessionTo, + truncateAllTables, +} from "../../setup"; +import { parityId, seedParityRecord } from "./record-fixture"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); + +vi.mock("@/lib/rollups/measurement-rollups", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + ensureUserRollupsFresh: async (...args: unknown[]) => { + effectRecorder.record("ensureUserRollupsFresh", args); + }, + }; +}); + +vi.mock("@/lib/rollups/mood-rollups", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + ensureUserMoodRollupsFresh: async (...args: unknown[]) => { + effectRecorder.record("ensureUserMoodRollupsFresh", args); + }, + }; +}); + +const prisma = getPrismaClient(); +const DAY = 86_400_000; +const ANCHOR = startOfUtcToday(); +const NOW = new Date(ANCHOR.getTime() + 12 * 60 * 60 * 1000); +/** Days back on which a SQL window edge falls (see the header). */ +const EDGE_DAYS = [7, 30, 60]; + +const OWNER_ID = parityId("insightscomp", 1); +const SPARSE_ID = parityId("insightscomp", 2); +const EMPTY_ID = parityId("insightscomp", 3); +const MANAGER_ID = parityId("insightscomp", 4); +const READER_ID = parityId("insightscomp", 5); + +let owner: SessionFixture; +let sparse: RouteAuth; +let empty: RouteAuth; +let manager: SessionFixture; +let reader: SessionFixture; + +/** + * Keys of the aggregator's per-phase timings, which the clock and the load + * decide. Their presence is kept, their value is not. + */ +function timingKeys(value: unknown, out = new Set()): Set { + if (Array.isArray(value)) value.forEach((v) => timingKeys(v, out)); + else if (value && typeof value === "object") { + for (const [k, v] of Object.entries(value)) { + if (/_ms$/.test(k)) out.add(k); + timingKeys(v, out); + } + } + return out; +} + +/** + * A stale serve starts one rebuild and returns without waiting for it. The + * rebuild annotates the request's event too, and whether it gets there before + * the event is emitted is a race between two sets of queries. On that case + * only the cache outcome is kept, which the route decides alone. + */ +function cacheFactsOnly(facts: unknown): unknown { + const f = facts as { meta?: Record | null }; + const meta = Object.fromEntries( + Object.entries(f.meta ?? {}).filter(([k]) => k.startsWith("cache.")), + ); + return { ...f, action: "", meta }; +} + +/** + * The aggregator's sub-queries run in parallel and annotate as each one + * finishes, so the order of their keys in the event follows the database, not + * the code. The keys are sorted; what they hold is unchanged. + */ +function sortedMeta(facts: unknown): unknown { + const f = facts as { meta?: Record | null }; + if (!f.meta) return facts; + const meta = Object.fromEntries( + Object.entries(f.meta).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)), + ); + return { ...f, meta }; +} + +async function step( + name: string, + options: { auth?: RouteAuth; raced?: boolean } = {}, +): Promise { + wideEventRecorder.take(); + effectRecorder.take(); + const res = await callRoute(GET, { + method: "GET", + route: "/api/insights/comprehensive", + auth: options.auth ?? owner, + }); + // A stale serve leaves one rebuild running; it finishes before the + // snapshot so the next case starts from the same cache. + await settleBackgroundTasks(); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + const facts = events + .map(eventFacts) + .map(sortedMeta) + .map((f) => (options.raced ? cacheFactsOnly(f) : f)); + await expectParity( + `insights-comprehensive.${name}`, + stable( + relativeToAnchor( + { + status: res.status, + body: res.body, + events: facts, + effects: effectRecorder.take(), + }, + ANCHOR, + ), + { since: NOW, volatileKeys: [...timingKeys(facts)] }, + ), + ); +} + +/** Remove the record's measurements on the SQL window edge days. */ +async function dropEdgeDays(userId: string): Promise { + for (const d of EDGE_DAYS) { + await prisma.measurement.deleteMany({ + where: { + userId, + measuredAt: { + gte: new Date(ANCHOR.getTime() - d * DAY), + lt: new Date(ANCHOR.getTime() - (d - 1) * DAY), + }, + }, + }); + } +} + +/** Mood without rollups, pulse without a resting series, odd medications. */ +async function seedSparseRecord(userId: string): Promise { + const at = (d: number, hh: number, mm = 0) => + new Date(ANCHOR.getTime() - d * DAY + (hh * 60 + mm) * 60_000); + const rows = []; + for (let d = 1; d < 50; d++) { + if (EDGE_DAYS.includes(d)) continue; + rows.push( + { + userId, + type: "WEIGHT" as const, + value: 70 + (d % 5) * 0.3, + unit: "kg", + measuredAt: at(d, 3, 30), + }, + { + userId, + type: "BLOOD_PRESSURE_SYS" as const, + // Higher on the days the morning dose was skipped, so the + // continuity correlation has something to find. + value: 131 + ((d * 3) % 11) + (d % 5 === 0 ? 22 : 0), + unit: "mmHg", + measuredAt: at(d, 4, 10), + }, + { + userId, + type: "BLOOD_PRESSURE_DIA" as const, + value: 84 + ((d * 2) % 7), + unit: "mmHg", + measuredAt: at(d, 4, 10), + }, + { + userId, + type: "PULSE" as const, + value: 66 + (d % 9), + unit: "bpm", + measuredAt: at(d, 4, 0), + }, + { + userId, + type: "PULSE" as const, + value: 92 + (d % 5), + unit: "bpm", + measuredAt: at(d, 18, 0), + }, + { + userId, + type: "PULSE" as const, + value: 78 + (d % 7), + unit: "bpm", + measuredAt: at(d, 15, 0), + }, + ); + } + await prisma.measurement.createMany({ data: rows }); + const moods = ["LAUSIG", "SCHLECHT", "OKAY", "GUT", "SUPER_GUT"]; + const moodRows = []; + for (let d = 1; d < 30; d++) { + const loggedAt = at(d, 2, 15); + const score = 1 + ((d * 2) % 5); + moodRows.push({ + userId, + // 02:15 UTC is the evening before in Bogotá. + date: new Date(loggedAt.getTime() - 5 * 60 * 60 * 1000) + .toISOString() + .slice(0, 10), + mood: moods[score - 1], + score, + moodLoggedAt: loggedAt, + tz: "America/Bogota", + createdAt: loggedAt, + }); + } + await prisma.moodEntry.createMany({ data: moodRows }); + const created = new Date(ANCHOR.getTime() - 60 * DAY); + await prisma.medication.create({ + data: { + id: parityId("insightscompmed", 1), + userId, + name: "Amlodipine", + dose: "5mg", + createdAt: created, + schedules: { + create: [ + { + id: parityId("insightscompsch", 1), + windowStart: "07:00", + windowEnd: "09:00", + }, + ], + }, + categoryAssignment: { create: { category: "BLOOD_PRESSURE" } }, + }, + }); + await prisma.medication.create({ + data: { + id: parityId("insightscompmed", 2), + userId, + name: "No schedule", + dose: "1 tab", + createdAt: created, + }, + }); + await prisma.medication.create({ + data: { + id: parityId("insightscompmed", 3), + userId, + name: "Ibuprofen", + dose: "400mg", + asNeeded: true, + createdAt: created, + }, + }); + const intakes = []; + for (let d = 1; d <= 40; d++) { + if (d % 4 === 0 || EDGE_DAYS.includes(d)) continue; + intakes.push({ + id: parityId("insightscompie", d), + userId, + medicationId: parityId("insightscompmed", 1), + scheduledFor: at(d, 12, 0), + takenAt: d % 5 === 0 ? null : at(d, 12, 30), + skipped: d % 5 === 0, + createdAt: at(d, 12, 0), + }); + } + await prisma.medicationIntakeEvent.createMany({ data: intakes }); +} + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + __resetAllCachesForTests(); + const ownerRow = await makeUser({ + id: OWNER_ID, + timezone: "UTC", + locale: "en", + heightCm: 178, + dateOfBirth: new Date("1975-04-02T00:00:00.000Z"), + gender: "male", + aiProvider: "ANTHROPIC", + aiModel: "claude-sonnet-4-6", + // Presence only: the overview never decrypts a key. + aiAnthropicKeyEncrypted: "v1:presence-only", + }); + await prisma.consentReceipt.create({ + data: { + userId: OWNER_ID, + kind: "ai_full", + artefact: "test", + signedAt: NOW, + }, + }); + await seedParityRecord({ userId: OWNER_ID, now: NOW, tz: "UTC", tag: "ic" }); + await dropEdgeDays(OWNER_ID); + const rollups = await vi.importActual< + typeof import("@/lib/rollups/measurement-rollups") + >("@/lib/rollups/measurement-rollups"); + await rollups.ensureUserRollupsFresh(OWNER_ID); + const moodRollups = await vi.importActual< + typeof import("@/lib/rollups/mood-rollups") + >("@/lib/rollups/mood-rollups"); + await moodRollups.ensureUserMoodRollupsFresh(OWNER_ID); + await settleBackgroundTasks(); + owner = await makeSession(ownerRow); + + const sparseRow = await makeUser({ + id: SPARSE_ID, + timezone: "America/Bogota", + locale: "de", + dateOfBirth: new Date("1950-08-20T00:00:00.000Z"), + }); + await seedSparseRecord(SPARSE_ID); + // Measurement rollups only: the mood tier stays empty on purpose. + await rollups.ensureUserRollupsFresh(SPARSE_ID); + await settleBackgroundTasks(); + sparse = await makeSession(sparseRow); + + const emptyRow = await makeUser({ id: EMPTY_ID, timezone: "UTC" }); + empty = await makeSession(emptyRow); + + const managerRow = await makeUser({ id: MANAGER_ID, timezone: "UTC" }); + await makeGrant(ownerRow, managerRow, { access: "MANAGE" }); + manager = await makeSession(managerRow); + await switchSessionTo(manager.sessionId, OWNER_ID); + + const readerRow = await makeUser({ id: READER_ID, timezone: "UTC" }); + await makeGrant(ownerRow, readerRow, { access: "READ" }); + reader = await makeSession(readerRow); + await switchSessionTo(reader.sessionId, OWNER_ID); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("GET /api/insights/comprehensive parity", () => { + it("01 an anonymous call", async () => { + await step("01-anonymous", { auth: anonymous }); + }); + + it("02 the overview, cold", async () => { + await step("02-cold"); + }); + + it("03 the overview, warm", async () => { + await step("03-warm"); + }); + + it("04 the overview, stale after a measurement write", async () => { + invalidateUserMeasurements(OWNER_ID); + await step("04-stale", { raced: true }); + }); + + it("05 the ai block after the AI analysis opt-out", async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { modulePreferencesJson: { insights: false } }, + }); + await step("05-ai-analysis-off"); + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { modulePreferencesJson: {} }, + }); + }); + + it("06 the ai block with the operator's master switch off", async () => { + await prisma.appSettings.upsert({ + where: { id: "singleton" }, + create: { id: "singleton", assistantEnabled: false }, + update: { assistantEnabled: false }, + }); + await step("06-operator-off"); + await prisma.appSettings.update({ + where: { id: "singleton" }, + data: { assistantEnabled: true }, + }); + }); + + it("07 a delegate with MANAGE access, cold", async () => { + __resetAllCachesForTests(); + await step("07-manage-delegate", { auth: manager }); + }); + + it("08 a delegate with READ access", async () => { + await step("08-read-delegate", { auth: reader }); + }); + + it("09 the shared analytics read ceiling", async () => { + await prisma.$executeRaw` + INSERT INTO rate_limits (key, count, reset_at) + VALUES (${`analytics-read:${OWNER_ID}`}, 100000, (NOW() AT TIME ZONE 'UTC') + INTERVAL '1 hour') + ON CONFLICT (key) DO UPDATE SET count = 100000`; + await step("09-rate-limited"); + await prisma.$executeRaw`DELETE FROM rate_limits WHERE key = ${`analytics-read:${OWNER_ID}`}`; + }); + + it("10 a sparse record west of UTC", async () => { + await step("10-sparse", { auth: sparse }); + }); + + it("11 an account with no data", async () => { + await step("11-empty", { auth: empty }); + }); +}); diff --git a/tests/integration/routes/parity/insights-generate.parity.test.ts b/tests/integration/routes/parity/insights-generate.parity.test.ts new file mode 100644 index 000000000..86b33a7fd --- /dev/null +++ b/tests/integration/routes/parity/insights-generate.parity.test.ts @@ -0,0 +1,1017 @@ +/** + * Parity for `GET` and `POST /api/insights/generate` while the briefing read + * and generation move out of the route into `src/lib/insights/`. + * + * One account with a full record (see `record-fixture.ts`) and its own + * Anthropic key walks through every case the two handlers tell apart. Each + * case starts from a known briefing state (`resetBriefing`): the cached + * payload, its stamp and language, the failure markers, the day's token + * ledger and the hourly generation quota are written for that case alone. + * + * The read: refusals, an empty cache, a fresh, a stale, an other-language, + * an unparseable and a legacy cache, a held failure and a withheld + * (ungrounded) briefing. The generation: the cache short-circuit and its + * bypasses, the body cap, the hourly quota with and without the cached + * fallback, a chain that resolves to nothing, the three rungs of the payload + * downgrade ladder, the day's token ceiling, every provider failure class + * the route maps, invalid and truncated replies, the grounding gate (retry + * kept, previous briefing stood in, stripped, retry unparseable), the + * outbound screen, both scope races, and successes in two languages, one + * with the comparison snapshot, an about-me block and the short form. + * + * The provider is never called: `runRawCompletionWithFallback` is replaced + * by a script each case fills, and every call it receives is recorded (the + * chain, the parameters, and a digest of the prompt). The queue enqueues and + * the failure markers the route does not await are recorded too; the markers + * are also written, and waited for before the snapshot. + * + * The clock is pinned (`Date` only) to noon of the day the test runs, and + * every instant and day key in the snapshot is written relative to the start + * of the day. The record's measurements on the 7th, 30th and 60th day back + * are removed, so a window cut in SQL against the database clock never meets + * a reading on its edge. Two values change from one day to the next by + * design and are normalised here: the opener hint and the first-name line of + * the briefing prompt rotate per user and day, and the stored snapshot hash + * covers dated features, so it is recorded as the hash of the same input + * with its dates written relative to the anchor. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { GET, POST } from "@/app/api/insights/generate/route"; +import { encrypt } from "@/lib/crypto"; +import { AllProvidersFailedError } from "@/lib/ai/provider-runner"; +import { + insightsCacheWrite, + openInsightsCachedText, +} from "@/lib/insights/cached-text-cipher"; +import { BRIEFING_GENERATED_AT_KEY } from "@/lib/insights/briefing-generated-at"; +import { settleBackgroundTasks } from "@/lib/logging/background-tasks"; + +import { anonymous, callRoute, type RouteAuth } from "../../../kit/call-route"; +import { makeSession, makeUser } from "../../../kit/factories"; +import { + effectRecorder, + eventFacts, + expectParity, + relativeToAnchor, + stable, + startOfUtcToday, + wideEventRecorder, +} from "../../../kit/parity"; +import { getPrismaClient, truncateAllTables } from "../../setup"; +import { parityId, seedParityRecord } from "./record-fixture"; + +/** What the provider stand-in answers, one entry per call, in order. */ +type ScriptEntry = + | { + content: string; + finishReason?: string; + tokensUsed?: number; + /** Runs inside the call, before the answer: a write racing the request. */ + during?: () => Promise; + } + | { error: () => Error }; + +const seams = vi.hoisted(() => ({ + script: [] as ScriptEntry[], + /** Feature reads that fail as oversized before computing, in order. */ + oversize: [] as number[], + /** Real snapshot hash → the same input's hash with anchored dates. */ + hashes: new Map(), + pending: [] as Promise[], + /** A provider call no case scripted: the case is wrong, not the route. */ + unscripted: 0, +})); + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); + +vi.mock("@/lib/rollups/measurement-rollups", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + ensureUserRollupsFresh: async (...args: unknown[]) => { + effectRecorder.record("ensureUserRollupsFresh", args); + }, + }; +}); + +vi.mock("@/lib/rollups/mood-rollups", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + ensureUserMoodRollupsFresh: async (...args: unknown[]) => { + effectRecorder.record("ensureUserMoodRollupsFresh", args); + }, + }; +}); + +vi.mock("@/lib/ai/provider-runner", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder, relativeToAnchor, startOfUtcToday } = + await import("../../../kit/parity"); + return { + ...actual, + runRawCompletionWithFallback: async ( + args: Parameters[0], + ) => { + const { messages, system, signal: _signal, ...rest } = args.params; + const prompt = String( + relativeToAnchor( + `${system}\n---\n${messages + .map((m) => + typeof m.content === "string" + ? m.content + : JSON.stringify(m.content), + ) + .join("\n---\n")}`, + startOfUtcToday(), + ), + ) + .replace(/^(OPENER HINT|OPENER-HINWEIS): .*$/gm, "$1: ") + .replace(/\nNAME: [^\n]*/g, ""); + const { createHash: hash } = await import("node:crypto"); + effectRecorder.record("provider", [ + { + surface: args.surface, + chain: args.providers.map((p) => p.providerType), + params: rest, + promptChars: prompt.length, + promptSha256: hash("sha256").update(prompt).digest("hex"), + }, + ]); + const next = seams.script.shift(); + if (!next) { + seams.unscripted += 1; + throw new Error("parity: an unscripted provider call"); + } + if ("error" in next) throw next.error(); + if (next.during) await next.during(); + return { + result: { + content: next.content, + tokensUsed: next.tokensUsed ?? 1200, + model: "claude-parity", + providerType: "anthropic", + finishReason: next.finishReason ?? "stop", + }, + workingProvider: args.providers[0], + fallbackHops: [], + }; + }, + }; +}); + +vi.mock("@/lib/insights/features", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + extractFeatures: async ( + ...args: Parameters + ) => { + const size = seams.oversize.shift(); + if (size !== undefined) { + throw new actual.FeaturesPayloadTooLargeError(size, 5_000_000); + } + return actual.extractFeatures(...args); + }, + }; +}); + +vi.mock("@/lib/insights/snapshot-hash", async (importOriginal) => { + const actual = + await importOriginal(); + const { relativeToAnchor, startOfUtcToday } = + await import("../../../kit/parity"); + return { + ...actual, + hashInsightSnapshot: (value: unknown) => { + const real = actual.hashInsightSnapshot(value); + if (value && typeof value === "object" && "features" in value) { + seams.hashes.set( + real, + actual.hashInsightSnapshot( + relativeToAnchor(value, startOfUtcToday()), + ), + ); + } + return real; + }, + }; +}); + +vi.mock("@/lib/insights/briefing-failure-marker", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/insights/briefing-failure-marker") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + recordBriefingFailure: ( + args: Parameters[0], + ) => { + effectRecorder.record("recordBriefingFailure", [args]); + const written = actual.recordBriefingFailure(args); + seams.pending.push(written); + return written; + }, + }; +}); + +vi.mock("@/lib/jobs/insight-pregenerate-shared", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/jobs/insight-pregenerate-shared") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + enqueueForceWarm: async (...args: unknown[]) => { + effectRecorder.record("enqueueForceWarm", args); + }, + }; +}); + +vi.mock("@/lib/jobs/insight-status-generate-shared", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/jobs/insight-status-generate-shared") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + enqueueStatusGeneration: async (...args: unknown[]) => { + effectRecorder.record("enqueueStatusGeneration", args); + }, + }; +}); + +const prisma = getPrismaClient(); +const DAY = 86_400_000; +const HOUR = 3_600_000; +const ANCHOR = startOfUtcToday(); +const NOW = new Date(ANCHOR.getTime() + 12 * HOUR); +const EDGE_DAYS = [7, 30, 60]; +const OWNER_ID = parityId("insightsgen", 1); +const OPTED_OUT_ID = parityId("insightsgen", 2); +const UNREADABLE_KEY_ID = parityId("insightsgen", 3); + +let owner: RouteAuth; +let optedOut: RouteAuth; +let unreadableKey: RouteAuth; + +// ── Payloads ──────────────────────────────────────────────── + +const GROUNDED_BRIEFING = { + paragraph: "Your readings held steady this week, with no surprises.", + signalsOfDay: [ + { + sourceMetric: "weight", + tone: "info", + headline: "Weight is steady", + nudge: "Keep weighing in at the same time each morning.", + delta: null, + }, + ], + keyFindings: [], +}; + +const UNGROUNDED_BRIEFING = { + paragraph: "Your weight reached 123.4 kg this morning, up 9.7 kg.", + signalsOfDay: [], + keyFindings: [], +}; + +const PREVIOUS_BRIEFING = { + paragraph: "Yesterday's briefing, grounded when it was written.", + signalsOfDay: [], + keyFindings: [], +}; + +/** + * A reply in the rich shape the strict prompt asks for. It carries no + * `disclaimer`, so the strict schema rejects it and the route keeps the + * parsed object whole, briefing included. + */ +function reply(briefing: unknown, extra: Record = {}) { + return JSON.stringify({ + summary: "Your tracked metrics held steady over the last 30 days.", + classification: "gut", + findings: [], + correlations: [], + recommendations: [], + dataQuality: { coverage: "good", gaps: [], confidence: "hoch" }, + dailyBriefing: briefing, + ...extra, + }); +} + +/** A cached payload as the generator stores it. */ +function cached(briefing: unknown, generatedAt: Date | null) { + return { + summary: "Cached summary.", + classification: "gut", + findings: [], + correlations: [], + recommendations: [], + dataQuality: { coverage: "good", gaps: [], confidence: "hoch" }, + dailyBriefing: briefing, + ...(generatedAt + ? { [BRIEFING_GENERATED_AT_KEY]: generatedAt.toISOString() } + : {}), + }; +} + +function hop(httpStatus: number | null, attempt = 1) { + return { + providerType: "anthropic" as const, + attempt, + failureReason: httpStatus === null ? "ECONNRESET" : `HTTP ${httpStatus}`, + httpStatus, + }; +} + +function providerError(httpStatus: number): () => Error { + return () => + Object.assign(new Error(`upstream answered ${httpStatus}`), { + httpStatus, + bodyExcerpt: `{"error":"status ${httpStatus}"}`, + }); +} + +// ── State ─────────────────────────────────────────────────── + +interface BriefingState { + /** The cached payload: an object, a raw string, or nothing. */ + text?: unknown; + cachedAt?: Date | null; + cachedLocale?: string | null; + /** Failure markers to write, with their reason and age. */ + markers?: Array<{ reason: string; at: Date; httpStatus?: number | null }>; + /** Tokens already spent today. */ + spentToday?: number; + /** Generations already counted against this hour's quota. */ + quotaUsed?: number; +} + +async function resetBriefing(userId: string, state: BriefingState = {}) { + const text = + state.text === undefined + ? null + : typeof state.text === "string" + ? state.text + : JSON.stringify(state.text); + await prisma.user.update({ + where: { id: userId }, + data: { + insightsCachedAt: state.cachedAt ?? null, + insightsCachedTextEncrypted: text + ? insightsCacheWrite(text).insightsCachedTextEncrypted + : null, + insightsCachedLocale: state.cachedLocale ?? null, + insightsSnapshotHash: null, + }, + }); + await prisma.auditLog.deleteMany({ where: { userId } }); + for (const m of state.markers ?? []) { + await prisma.auditLog.create({ + data: { + userId, + action: "insights.briefing-failure", + createdAt: m.at, + details: JSON.stringify({ + reason: m.reason, + httpStatus: m.httpStatus ?? null, + failureKind: null, + }), + }, + }); + } + await prisma.coachUsage.deleteMany({ where: { userId } }); + if (state.spentToday) { + await prisma.coachUsage.create({ + data: { + userId, + dateKey: NOW.toISOString().slice(0, 10), + totalTokens: state.spentToday, + }, + }); + } + await prisma.rateLimit.deleteMany({ where: { key: `insights:${userId}` } }); + if (state.quotaUsed) { + await prisma.$executeRaw` + INSERT INTO rate_limits (key, count, reset_at) + VALUES (${`insights:${userId}`}, ${state.quotaUsed}, (NOW() AT TIME ZONE 'UTC') + INTERVAL '1 hour')`; + } + seams.script.length = 0; + seams.oversize.length = 0; +} + +async function stored(userId: string) { + const [user, audit, usage] = await Promise.all([ + prisma.user.findUniqueOrThrow({ + where: { id: userId }, + select: { + insightsCachedAt: true, + insightsCachedTextEncrypted: true, + insightsCachedLocale: true, + insightsSnapshotHash: true, + }, + }), + prisma.auditLog.findMany({ + where: { userId }, + orderBy: [{ createdAt: "asc" }, { action: "asc" }], + select: { action: true, details: true, ipAddress: true }, + }), + prisma.coachUsage.findMany({ + where: { userId }, + select: { + dateKey: true, + totalTokens: true, + operatorTokens: true, + messageCount: true, + }, + }), + ]); + const text = openInsightsCachedText(user); + let payload: unknown = text; + try { + payload = text === null ? null : JSON.parse(text); + } catch { + // Kept as the raw string. + } + const hash = user.insightsSnapshotHash; + return { + insightsCachedAt: user.insightsCachedAt, + insightsCachedLocale: user.insightsCachedLocale, + cachedPayload: payload, + snapshotHash: + hash === null ? null : (seams.hashes.get(hash) ?? ""), + audit: audit.map((row) => ({ + ...row, + details: row.details === null ? null : JSON.parse(row.details), + })), + usage, + }; +} + +/** + * The feature read annotates from parallel queries, so the order of its meta + * keys follows the database. The keys are sorted; what they hold is not + * touched. + */ +function sortedMeta(facts: unknown): unknown { + const f = facts as { meta?: Record | null }; + if (!f.meta) return facts; + return { + ...f, + meta: Object.fromEntries( + Object.entries(f.meta).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)), + ), + }; +} + +/** Keys of per-phase timings, which the clock and the load decide. */ +function timingKeys(value: unknown, out = new Set()): Set { + if (Array.isArray(value)) value.forEach((v) => timingKeys(v, out)); + else if (value && typeof value === "object") { + for (const [k, v] of Object.entries(value)) { + if (/_ms$|Ms$/.test(k)) out.add(k); + timingKeys(v, out); + } + } + return out; +} + +async function step( + name: string, + options: { + method: "GET" | "POST"; + auth?: RouteAuth; + body?: unknown; + userId?: string; + headers?: Record; + /** A status the contract does not declare; the reason says which. */ + undeclared?: string; + }, +): Promise { + wideEventRecorder.take(); + effectRecorder.take(); + const handler = options.method === "GET" ? GET : POST; + const res = await callRoute(handler, { + method: options.method, + route: "/api/insights/generate", + body: options.body, + headers: options.headers, + auth: options.auth ?? owner, + contract: options.undeclared ? { skip: options.undeclared } : undefined, + }); + await Promise.all(seams.pending.splice(0)); + await settleBackgroundTasks(); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + const facts = events.map(eventFacts).map(sortedMeta); + expect(seams.script, "every scripted reply was asked for").toEqual([]); + expect(seams.unscripted, "no provider call went unscripted").toBe(0); + expect(seams.oversize, "every planted oversize was read").toEqual([]); + await expectParity( + `insights-generate.${name}`, + stable( + relativeToAnchor( + { + status: res.status, + body: res.body, + events: facts, + effects: effectRecorder.take(), + stored: await stored(options.userId ?? OWNER_ID), + }, + ANCHOR, + ), + { since: NOW, volatileKeys: [...timingKeys(facts)] }, + ), + ); +} + +const get = (name: string, opts: { auth?: RouteAuth; userId?: string } = {}) => + step(name, { method: "GET", ...opts }); +const post = ( + name: string, + body: unknown = { force: true }, + opts: { auth?: RouteAuth; userId?: string; undeclared?: string } = {}, +) => step(name, { method: "POST", body, ...opts }); + +async function dropEdgeDays(userId: string): Promise { + for (const d of EDGE_DAYS) { + await prisma.measurement.deleteMany({ + where: { + userId, + measuredAt: { + gte: new Date(ANCHOR.getTime() - d * DAY), + lt: new Date(ANCHOR.getTime() - (d - 1) * DAY), + }, + }, + }); + } +} + +async function consent(userId: string): Promise { + await prisma.consentReceipt.create({ + data: { userId, kind: "ai_full", artefact: "test", signedAt: NOW }, + }); +} + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const ownerRow = await makeUser({ + id: OWNER_ID, + timezone: "UTC", + locale: "en", + heightCm: 178, + dateOfBirth: new Date("1975-04-02T00:00:00.000Z"), + gender: "male", + displayName: "Parity Person", + aiProvider: "ANTHROPIC", + aiModel: "claude-sonnet-4-6", + aiAnthropicKeyEncrypted: encrypt("sk-ant-parity"), + }); + await consent(OWNER_ID); + await seedParityRecord({ userId: OWNER_ID, now: NOW, tz: "UTC", tag: "ig" }); + await dropEdgeDays(OWNER_ID); + const rollups = await vi.importActual< + typeof import("@/lib/rollups/measurement-rollups") + >("@/lib/rollups/measurement-rollups"); + await rollups.ensureUserRollupsFresh(OWNER_ID); + const moodRollups = await vi.importActual< + typeof import("@/lib/rollups/mood-rollups") + >("@/lib/rollups/mood-rollups"); + await moodRollups.ensureUserMoodRollupsFresh(OWNER_ID); + await settleBackgroundTasks(); + owner = await makeSession(ownerRow); + + const optedOutRow = await makeUser({ + id: OPTED_OUT_ID, + timezone: "UTC", + locale: "en", + modulePreferencesJson: { insights: false }, + aiProvider: "ANTHROPIC", + aiModel: "claude-sonnet-4-6", + aiAnthropicKeyEncrypted: encrypt("sk-ant-parity"), + }); + await consent(OPTED_OUT_ID); + optedOut = await makeSession(optedOutRow); + + // A key the presence probe sees and no resolver can open. + const unreadableRow = await makeUser({ + id: UNREADABLE_KEY_ID, + timezone: "UTC", + locale: "en", + aiProvider: "ANTHROPIC", + aiModel: "claude-sonnet-4-6", + aiAnthropicKeyEncrypted: "v1:presence-only", + }); + await consent(UNREADABLE_KEY_ID); + unreadableKey = await makeSession(unreadableRow); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("GET /api/insights/generate parity", () => { + it("G01 an anonymous call", async () => { + await get("G01-anonymous", { auth: anonymous }); + }); + + it("G02 the AI analysis opt-out", async () => { + await get("G02-opted-out", { auth: optedOut, userId: OPTED_OUT_ID }); + }); + + it("G03 no cached briefing", async () => { + await resetBriefing(OWNER_ID); + await get("G03-empty"); + }); + + it("G04 a briefing written today", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "en", + }); + await get("G04-fresh"); + }); + + it("G05 a briefing written yesterday", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - 20 * HOUR)), + cachedAt: new Date(NOW.getTime() - 2 * HOUR), + cachedLocale: "en", + }); + await get("G05-stale"); + }); + + it("G06 a briefing in another language", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "de", + }); + await get("G06-other-language"); + }); + + it("G07 an unparseable cache row", async () => { + await resetBriefing(OWNER_ID, { + text: "{not json", + cachedAt: new Date(NOW.getTime() - HOUR), + }); + await get("G07-unparseable"); + }); + + it("G08 a legacy payload, untagged", async () => { + await resetBriefing(OWNER_ID, { + text: { summary: "Legacy", recommendations: ["Drink water."] }, + cachedAt: new Date(NOW.getTime() - HOUR), + }); + await get("G08-legacy"); + }); + + it("G09 a failure newer than the last good briefing", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - 3 * HOUR)), + cachedAt: new Date(NOW.getTime() - 3 * HOUR), + cachedLocale: "en", + markers: [ + { + reason: "all-providers-failed", + at: new Date(NOW.getTime() - HOUR), + httpStatus: 429, + }, + ], + }); + await get("G09-held-after-failure"); + }); + + it("G10 a failure with no good briefing, and an older superseded one", async () => { + await resetBriefing(OWNER_ID, { + markers: [ + { reason: "provider-error", at: new Date(NOW.getTime() - 5 * HOUR) }, + { reason: "invalid-json", at: new Date(NOW.getTime() - HOUR) }, + ], + }); + await get("G10-failed-empty"); + }); + + it("G11 a briefing the grounding gate withheld", async () => { + await resetBriefing(OWNER_ID, { + text: cached(null, new Date(NOW.getTime() - 2 * HOUR)), + cachedAt: new Date(NOW.getTime() - 2 * HOUR), + cachedLocale: "en", + markers: [ + { reason: "briefing-ungrounded", at: new Date(NOW.getTime() - HOUR) }, + ], + }); + await get("G11-withheld"); + }); +}); + +describe("POST /api/insights/generate parity", () => { + it("P01 an anonymous call", async () => { + await post("P01-anonymous", { force: true }, { auth: anonymous }); + }); + + it("P02 the AI analysis opt-out", async () => { + await post( + "P02-opted-out", + { force: true }, + { auth: optedOut, userId: OPTED_OUT_ID }, + ); + }); + + it("P03 a body over the cap", async () => { + await resetBriefing(OWNER_ID); + await post("P03-body-too-large", { pad: "x".repeat(17 * 1024) }); + }); + + it("P04 a cached briefing written today", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "en", + }); + await post("P04-cache-hit", {}); + }); + + it("P05 a cached briefing from yesterday, regenerated", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - 20 * HOUR)), + cachedAt: new Date(NOW.getTime() - 2 * HOUR), + cachedLocale: "en", + }); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P05-yesterday-regenerated", {}); + }); + + it("P06 a cache in another language, regenerated", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "de", + }); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P06-other-language-regenerated", {}); + }); + + it("P07 a briefingless cache when the hour's quota is spent", async () => { + await resetBriefing(OWNER_ID, { + text: cached(null, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "en", + quotaUsed: 10, + }); + await post("P07-quota-cached-fallback", {}); + }); + + it("P08 a forced generation when the hour's quota is spent", async () => { + await resetBriefing(OWNER_ID, { quotaUsed: 10 }); + await post("P08-quota-refused"); + }); + + it("P09 a key no resolver can open", async () => { + await resetBriefing(UNREADABLE_KEY_ID); + await post( + "P09-unreadable-key", + { force: true }, + { + auth: unreadableKey, + userId: UNREADABLE_KEY_ID, + undeclared: "an unreadable stored key ends in 500 (pinned as it is)", + }, + ); + }); + + it("P10 a fresh generation", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ content: reply(GROUNDED_BRIEFING), tokensUsed: 2100 }); + await post("P10-generated"); + }); + + it("P11 a reply the strict schema accepts", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: reply(GROUNDED_BRIEFING, { disclaimer: "Consult your doctor." }), + }); + await post("P11-strict-reply"); + }); + + it("P12 a generation in German with the comparison, about-me and short form", async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { + locale: "de", + aiUsageLevel: "FRUGAL", + dashboardWidgetsJson: { comparisonBaseline: "lastMonth" }, + }, + }); + await prisma.allergy.create({ + data: { userId: OWNER_ID, substance: "Penicillin" }, + }); + await resetBriefing(OWNER_ID); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P12-generated-de-short"); + await prisma.allergy.deleteMany({ where: { userId: OWNER_ID } }); + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { locale: "en", aiUsageLevel: null, dashboardWidgetsJson: {} }, + }); + }); + + it("P13 an oversized raw payload, downgraded once", async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { insightsPrivacyMode: "raw" }, + }); + await resetBriefing(OWNER_ID); + seams.oversize.push(6_100_000); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P13-downgraded"); + }); + + it("P14 an oversized aggregate too, downgraded twice", async () => { + await resetBriefing(OWNER_ID); + seams.oversize.push(6_100_000, 5_400_000); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P14-hard-downgraded"); + }); + + it("P15 oversized on every rung", async () => { + await resetBriefing(OWNER_ID); + seams.oversize.push(6_100_000, 5_400_000, 5_200_000); + await post("P15-too-large"); + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { insightsPrivacyMode: "aggregated" }, + }); + }); + + it("P16 the day's token ceiling", async () => { + await resetBriefing(OWNER_ID, { spentToday: 5_000_000 }); + await post("P16-budget-exceeded"); + }); + + it("P17 every provider refused the credential", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + error: () => new AllProvidersFailedError([hop(401), hop(403, 2)]), + }); + await post("P17-chain-auth"); + }); + + it("P18 every provider rate-limited", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ error: () => new AllProvidersFailedError([hop(429)]) }); + await post("P18-chain-rate-limited"); + }); + + it("P19 a provider in a 5xx", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + error: () => new AllProvidersFailedError([hop(401), hop(502, 2)]), + }); + await post("P19-chain-5xx"); + }); + + it("P20 every provider unreachable", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + error: () => new AllProvidersFailedError([hop(null), hop(null, 2)]), + }); + await post("P20-chain-transport"); + }); + + it("P21 a mixed chain failure", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + error: () => new AllProvidersFailedError([hop(404), hop(429, 2)]), + }); + await post("P21-chain-mixed"); + }); + + it("P22 one provider error per status class", async () => { + for (const status of [401, 429, 500, 400]) { + await resetBriefing(OWNER_ID); + seams.script.push({ error: providerError(status) }); + await post(`P22-provider-${status}`); + } + }); + + it("P23 a reply that is not JSON", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ content: "Sure! Here is your briefing." }); + await post("P23-invalid-json"); + }); + + it("P24 a reply cut off at the token ceiling", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ content: '{"summary": "Your', finishReason: "length" }); + await post("P24-truncated"); + }); + + it("P25 an ungrounded briefing the retry fixes", async () => { + await resetBriefing(OWNER_ID); + seams.script.push( + { content: reply(UNGROUNDED_BRIEFING) }, + { content: reply(GROUNDED_BRIEFING), tokensUsed: 900 }, + ); + await post("P25-grounding-retry-kept"); + }); + + it("P26 still ungrounded, the previous briefing stands in", async () => { + await resetBriefing(OWNER_ID, { + text: cached(PREVIOUS_BRIEFING, new Date(NOW.getTime() - 26 * HOUR)), + cachedAt: new Date(NOW.getTime() - 26 * HOUR), + cachedLocale: "en", + }); + seams.script.push( + { content: reply(UNGROUNDED_BRIEFING) }, + { content: reply(UNGROUNDED_BRIEFING) }, + ); + await post("P26-grounding-previous"); + }); + + it("P27 still ungrounded, nothing to stand in", async () => { + await resetBriefing(OWNER_ID); + seams.script.push( + { content: reply(UNGROUNDED_BRIEFING) }, + { content: reply(UNGROUNDED_BRIEFING) }, + ); + await post("P27-grounding-stripped"); + }); + + it("P28 an ungrounded briefing whose retry is not JSON", async () => { + await resetBriefing(OWNER_ID); + seams.script.push( + { content: reply(UNGROUNDED_BRIEFING) }, + { content: "not json" }, + ); + await post("P28-grounding-retry-unparseable"); + }); + + it("P29 a reply the outbound screen blocks", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: reply(GROUNDED_BRIEFING, { + summary: + "Your 10-year cardiovascular risk is elevated — consider stepping your dose up.", + }), + }); + await post("P29-outbound-screened", { force: true }); + }); + + it("P30 the about-me text changing during the generation", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: reply(GROUNDED_BRIEFING), + during: async () => { + await prisma.allergy.create({ + data: { userId: OWNER_ID, substance: "Latex" }, + }); + }, + }); + await post("P30-profile-scope-changed"); + await prisma.allergy.deleteMany({ where: { userId: OWNER_ID } }); + }); + + it("P31 the profile changing during the generation", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: reply(GROUNDED_BRIEFING), + during: async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { displayName: "Renamed Person" }, + }); + }, + }); + await post("P31-scope-changed"); + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { displayName: "Parity Person" }, + }); + }); +}); diff --git a/tests/integration/routes/parity/measurement-series.parity.test.ts b/tests/integration/routes/parity/measurement-series.parity.test.ts new file mode 100644 index 000000000..3c2e82bc6 --- /dev/null +++ b/tests/integration/routes/parity/measurement-series.parity.test.ts @@ -0,0 +1,279 @@ +/** + * Parity for `GET /api/measurements/series` while its readers move into + * `src/lib/measurements/series/`. + * + * One account holds weight, blood pressure from two sources (with a corrupt + * systolic row), pulse with hourly spreads, a stream of per-minute pulse + * dense enough to be bucketed per hour, glucose in mmol/L preference, sleep + * stages and steps. The walk asks for each kind it reads differently: the + * plain readers, the paired blood pressure, the sleep nights, a dense kind + * inside the raw window, the hour buckets, the day buckets of a long window, + * a refused query, and a delegate reading the record in another language. + * + * The clock is NOT pinned here: the series readers cut their windows in SQL + * against the database's own `now()`, which a pinned `Date` would disagree + * with. The record is laid out relative to the start of the current UTC day + * instead, no reading sits on a window's edge day, and every instant and day + * key in the snapshot is written relative to that start, so the snapshot is + * the same on every day it runs. + */ +import { beforeAll, describe, expect, it, vi } from "vitest"; + +import { GET } from "@/app/api/measurements/series/route"; +import type { Prisma } from "@/generated/prisma/client"; + +import { callRoute, type RouteAuth } from "../../../kit/call-route"; +import { makeGrant, makeSession, makeUser } from "../../../kit/factories"; +import { + eventFacts, + expectParity, + relativeToAnchor, + stable, + startOfUtcToday, + wideEventRecorder, +} from "../../../kit/parity"; +import { + getPrismaClient, + switchSessionTo, + truncateAllTables, +} from "../../setup"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); + +const prisma = getPrismaClient(); +const DAY = 86_400_000; +/** The start of the current UTC day: the anchor the record is laid out from. */ +const ANCHOR = startOfUtcToday(); +/** + * Days back the daily readings sit. The windows asked for are 7, 14, 30 and + * 365 days; a reading on exactly that many days back would fall in or out of + * the window with the time of day the test runs, so those days are skipped. + */ +const READING_DAYS = Array.from({ length: 20 }, (_, i) => i + 1).filter( + (d) => d !== 7 && d !== 14, +); + +let userId: string; +let owner: RouteAuth; +let delegate: { auth: RouteAuth & { sessionId: string } }; + +function at(daysAgo: number, hour: number, minute = 0): Date { + return new Date( + ANCHOR.getTime() - daysAgo * DAY + (hour * 60 + minute) * 60_000, + ); +} + +function seedRows(): Prisma.MeasurementCreateManyInput[] { + const rows: Prisma.MeasurementCreateManyInput[] = []; + const add = (row: Omit) => + rows.push({ userId, ...row }); + for (const d of READING_DAYS) { + add({ + type: "WEIGHT", + value: 80 - d * 0.1, + unit: "kg", + measuredAt: at(d, 6, 30), + source: d % 2 ? "MANUAL" : "WITHINGS", + externalId: d % 2 ? null : `w-${d}`, + }); + for (const [source, offset] of [ + ["MANUAL", 0], + ["WITHINGS", 1], + ] as const) { + add({ + type: "BLOOD_PRESSURE_SYS", + value: 125 + (d % 5) + offset, + unit: "mmHg", + measuredAt: at(d, 7, offset), + source, + externalId: source === "WITHINGS" ? `bp-s-${d}` : null, + }); + add({ + type: "BLOOD_PRESSURE_DIA", + value: 80 + (d % 3) + offset, + unit: "mmHg", + measuredAt: at(d, 7, offset + 2), + source, + externalId: source === "WITHINGS" ? `bp-d-${d}` : null, + }); + } + add({ + type: "BLOOD_GLUCOSE", + value: 95 + d, + unit: "mg/dL", + measuredAt: at(d, 8), + source: "MANUAL", + }); + add({ + type: "ACTIVITY_STEPS", + value: 6_000 + d * 100, + unit: "steps", + measuredAt: at(d, 12), + source: "APPLE_HEALTH", + externalId: `stats:steps:${d}`, + aggregationProvenance: "HEALTHKIT_STATISTICS", + }); + for (const [stage, start, minutes] of [ + ["CORE", 23, 180], + ["DEEP", 2, 60], + ["REM", 3, 90], + ["AWAKE", 4, 10], + ] as const) { + const begin = at(d, start); + if (start === 23) begin.setTime(begin.getTime() - DAY); + add({ + type: "SLEEP_DURATION", + value: minutes, + unit: "min", + measuredAt: new Date(begin.getTime() + minutes * 60_000), + source: "APPLE_HEALTH", + sleepStage: stage, + externalId: `sleep-${d}-${stage}`, + }); + } + for (const hour of [9, 14, 20]) { + add({ + type: "PULSE", + value: 60 + hour, + valueMin: 55 + hour, + valueMax: 70 + hour, + unit: "bpm", + measuredAt: at(d, hour), + source: "APPLE_HEALTH", + externalId: `stats:hr:${d}:${hour}`, + aggregationProvenance: "HEALTHKIT_STATISTICS", + }); + } + } + // A seed-era corrupt systolic the series must never surface. + add({ + type: "BLOOD_PRESSURE_SYS", + value: 0, + unit: "mmHg", + measuredAt: at(2, 9), + source: "MANUAL", + }); + // Two months back, so a long window has more than the raw window holds. + for (let d = 60; d <= 70; d++) { + add({ + type: "BLOOD_GLUCOSE", + value: 110, + unit: "mg/dL", + measuredAt: at(d, 8), + source: "MANUAL", + }); + } + return rows; +} + +/** A per-minute heart-rate stream dense enough to be bucketed per hour. */ +function denseHeartRate(): Prisma.MeasurementCreateManyInput[] { + return Array.from({ length: 10_200 }, (_, i) => ({ + userId, + type: "HEART_RATE_VARIABILITY", + value: 40 + (i % 17), + unit: "ms", + measuredAt: new Date(ANCHOR.getTime() - (i + 1) * 60_000), + source: "APPLE_HEALTH", + externalId: `hrv-${i}`, + })); +} + +async function step( + name: string, + query: Record, + auth: RouteAuth = owner, +): Promise { + wideEventRecorder.take(); + const res = await callRoute(GET, { + method: "GET", + route: "/api/measurements/series", + query, + auth, + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + await expectParity( + `measurement-series.${name}`, + stable( + relativeToAnchor( + { + status: res.status, + body: res.body, + events: events.map(eventFacts), + }, + ANCHOR, + ), + { since: new Date() }, + ), + ); +} + +beforeAll(async () => { + await truncateAllTables(prisma); + const user = await makeUser({ + // UTC, so a daylight-saving change between two runs cannot move a local + // day boundary relative to the anchor. + timezone: "UTC", + glucoseUnit: "mmol/L", + dateOfBirth: new Date("1970-01-01T00:00:00.000Z"), + locale: "en", + }); + userId = user.id; + owner = await makeSession(user); + await prisma.measurement.createMany({ data: seedRows() }); + await prisma.measurement.createMany({ data: denseHeartRate() }); + const reader = await makeUser({ timezone: "UTC", locale: "de" }); + await makeGrant(user, reader); + const session = await makeSession(reader); + delegate = { auth: session }; +}); + +describe("GET /api/measurements/series parity", () => { + it("the plain readers", async () => { + await step("01-weight", { kind: "weight" }); + await step("02-steps", { kind: "steps", days: 14 }); + await step("03-pulse-spread", { kind: "pulse", days: 7 }); + }); + + it("the paired blood pressure and the sleep nights", async () => { + await step("04-blood-pressure", { kind: "bloodPressure" }); + await step("05-sleep", { kind: "sleep", days: 14 }); + }); + + it("glucose raw, and day-bucketed past the raw window", async () => { + await step("06-glucose-raw", { kind: "glucose" }); + await step("07-glucose-long", { kind: "glucose", days: 365 }); + }); + + it("a dense stream bucketed per hour", async () => { + await step("08-hrv-hour-buckets", { + kind: "heartRateVariability", + days: 30, + }); + }); + + it("a refused query", async () => { + await step("09-invalid", { kind: "temperature", days: 0 }); + }); + + it("a delegate reading the record in another language", async () => { + await switchSessionTo(delegate.auth.sessionId, userId); + await step( + "10-delegated", + { kind: "bloodPressure", days: 7 }, + delegate.auth, + ); + await switchSessionTo(delegate.auth.sessionId, null); + }); +}); diff --git a/tests/integration/routes/parity/measurements.parity.test.ts b/tests/integration/routes/parity/measurements.parity.test.ts index 48f63850e..5055c39e0 100644 --- a/tests/integration/routes/parity/measurements.parity.test.ts +++ b/tests/integration/routes/parity/measurements.parity.test.ts @@ -378,17 +378,10 @@ describe("GET /api/measurements parity", () => { it("the list modes", async () => { await get("get-04-sleep", { type: "SLEEP_DURATION" }); - // The drill-down answers the stored rows as they are, `notesEncrypted` - // included, which the contract does not declare (the other list modes - // strip it). Pinned as it is; this walk records behaviour, it does not - // change it. - await get( - "get-05-drill-down", - { type: "ACTIVITY_STEPS", dayKey: "2026-10-13" }, - { - skip: "the drill-down returns notesEncrypted, which the contract omits", - }, - ); + await get("get-05-drill-down", { + type: "ACTIVITY_STEPS", + dayKey: "2026-10-13", + }); await get("get-06-day-grouped", { type: "ACTIVITY_STEPS", groupBy: "day" }); await get("get-07-rollup-series", { type: "ACTIVITY_STEPS", diff --git a/tests/integration/routes/parity/medication-detail.parity.test.ts b/tests/integration/routes/parity/medication-detail.parity.test.ts new file mode 100644 index 000000000..64b13ac07 --- /dev/null +++ b/tests/integration/routes/parity/medication-detail.parity.test.ts @@ -0,0 +1,579 @@ +/** + * Parity for `GET`, `PUT` and `DELETE /api/medications/{id}` while the read, + * the edit and the delete move into `src/lib/medications/`. + * + * One record walks through the cases the route tells apart, in order, each + * request seeing what the earlier ones stored. The reads: a rolling injection + * with stock, a composition, a custom category and automatic recording; a + * daily tablet without stock; a record-only medication; another account's + * medication; a reading delegate and nobody. The edits: plain fields, a + * schedule replace that moves the dose times outside the stale window (era + * archived, open slots tombstoned), the same schedule echoed back, the + * top-level reminder-window bridge, a pause and a resume, automatic recording + * switched on, the category writes (a custom one, a native client's OTHER over + * it, the web's OTHER, an unknown one), the record-only rule and both tracking + * transitions, the as-needed flips and their refusals, the one-shot refusals + * and a conversion, the course refusals and a course edit, a units change on + * a medication with a composition, a body the schema refuses, the not-found + * answer, a delegate who may write and one who may manage, and nobody. The + * deletes: the owner's (its scoped ingest token revoked), the not-found + * answer, a managing delegate and nobody. + * + * After each request the snapshot holds the response, the wide event, the + * recorded tails and every medication row of the accounts. The clock is + * pinned (`Date` only), so the snapshot is the same on every day it runs. + */ +import { afterAll, beforeAll, describe, it, vi } from "vitest"; + +import { DELETE, GET, PUT } from "@/app/api/medications/[id]/route"; +import { encryptToBytes } from "@/lib/ai/coach/bytes-codec"; + +import { anonymous, type RouteAuth } from "../../../kit/call-route"; +import { + makeMedication, + makeSession, + makeToken, + makeUser, +} from "../../../kit/factories"; +import { getPrismaClient, truncateAllTables } from "../../setup"; +import { delegateOf, parityStep } from "./medication-state"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); + +// The supplement recompute the route hands a composition change to, recorded +// instead of run (see `effectRecorder`). +vi.mock( + "@/lib/medications/ingredients/recompute-job", + async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/medications/ingredients/recompute-job") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + enqueueSupplementNutrientRecompute: async (...args: unknown[]) => { + effectRecorder.record("enqueueSupplementNutrientRecompute", args); + return { queued: true }; + }, + }; + }, +); + +const prisma = getPrismaClient(); +const NOW = new Date("2026-10-15T12:00:00.000Z"); +const ROUTE = "/api/medications/{id}"; +/** + * The edit answers without the read-model fields (reminder state, stock and + * runway, composition, links) the registered medication resource declares. + * The parity snapshot pins that answer as it is; the contract check is the + * route's own tests' business. + */ +const EDIT_ANSWER = { + skip: "the edit answers without the read-model fields of the resource", +}; +const CUSTOM_KEY = "custom:5b1f7c2e-4d3a-4b8e-9c61-2a7d0e9f4c13"; + +let ownerId: string; +let userIds: string[]; +let owner: RouteAuth; +let ownerBearer: RouteAuth; +let writer: { auth: RouteAuth; headers: Record }; +let reader: { auth: RouteAuth; headers: Record }; +let manager: { auth: RouteAuth; headers: Record }; +const med: Record = {}; + +type Caller = { auth?: RouteAuth; headers?: Record }; + +async function get(name: string, id: string, caller: Caller = {}) { + await parityStep({ + name: `medication-detail.${name}`, + handler: GET, + options: { + method: "GET", + route: ROUTE, + params: { id }, + auth: caller.auth ?? owner, + headers: caller.headers, + }, + userIds, + now: NOW, + }); +} + +async function put( + name: string, + id: string, + body: unknown, + caller: Caller = {}, +) { + await parityStep({ + name: `medication-detail.${name}`, + handler: PUT, + options: { + method: "PUT", + route: ROUTE, + params: { id }, + body, + auth: caller.auth ?? owner, + headers: caller.headers, + contract: EDIT_ANSWER, + }, + userIds, + now: NOW, + }); +} + +async function del(name: string, id: string, caller: Caller = {}) { + await parityStep({ + name: `medication-detail.${name}`, + handler: DELETE, + options: { + method: "DELETE", + route: ROUTE, + params: { id }, + auth: caller.auth ?? owner, + headers: caller.headers, + }, + userIds, + now: NOW, + // The ingest scope names the medication's generated id inside a string, + // which the id placeholders cannot see; it reads as the fixture's name. + extra: async () => + ( + await prisma.apiToken.findMany({ + where: { userId: ownerId }, + select: { name: true, permissions: true, revoked: true }, + orderBy: { name: "asc" }, + }) + ).map((token) => ({ + ...token, + permissions: token.permissions.map((p) => + p.replace(med.doomed, ""), + ), + })), + }); +} + +async function intake( + medicationId: string, + scheduledFor: string, + takenAt: string | null = null, +) { + await prisma.medicationIntakeEvent.create({ + data: { + userId: ownerId, + medicationId, + scheduledFor: new Date(scheduledFor), + takenAt: takenAt ? new Date(takenAt) : null, + createdAt: new Date("2026-10-01T06:00:00.000Z"), + }, + }); +} + +const daily = (windowStart: string, windowEnd: string, extra = {}) => ({ + windowStart, + windowEnd, + timesOfDay: [windowStart], + rrule: "FREQ=DAILY", + ...extra, +}); + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const user = await makeUser({ timezone: "Europe/Berlin", locale: "en" }); + ownerId = user.id; + owner = await makeSession(user); + ownerBearer = await makeToken(user, { name: "kit-phone" }); + const created = new Date("2026-10-01T06:00:00.000Z"); + + const rolling = await makeMedication( + user, + { + name: "Rolling pen", + dose: "1 pen", + deliveryForm: "INJECTION", + unitsPerDose: 1, + startsOn: new Date("2026-10-01"), + createdAt: created, + autoMarkTaken: true, + autoMarkTakenSince: created, + }, + [ + { + windowStart: "09:00", + windowEnd: "11:00", + timesOfDay: ["09:00"], + rollingIntervalDays: 7, + }, + ], + ); + med.rolling = rolling.id; + await intake( + rolling.id, + "2026-10-10T07:00:00.000Z", + "2026-10-10T07:05:00.000Z", + ); + await prisma.medicationInventoryItem.create({ + data: { + userId: ownerId, + medicationId: rolling.id, + unitsTotal: 4, + unitsRemaining: 3, + purchasedAt: created, + }, + }); + await prisma.medicationIngredient.create({ + data: { + userId: ownerId, + medicationId: rolling.id, + nutrient: "vitamin_d", + amountPerUnit: 25, + unit: "ug", + enteredAmount: 1000, + enteredUnit: "IU", + source: "MANUAL", + }, + }); + await prisma.medicationCategoryLabel.create({ + data: { + userId: ownerId, + key: CUSTOM_KEY, + labelEncrypted: encryptToBytes("Heart and circulation"), + }, + }); + await prisma.medicationCategoryAssignment.create({ + data: { medicationId: rolling.id, category: CUSTOM_KEY }, + }); + + const tablet = await makeMedication( + user, + { + name: "Daily tablet", + startsOn: new Date("2026-10-01"), + createdAt: created, + }, + [daily("08:00", "10:00"), daily("20:00", "22:00")], + ); + med.tablet = tablet.id; + await intake(tablet.id, "2026-10-14T18:00:00.000Z"); + await intake( + tablet.id, + "2026-10-15T06:00:00.000Z", + "2026-10-15T06:10:00.000Z", + ); + await intake(tablet.id, "2026-10-15T18:00:00.000Z"); + await intake(tablet.id, "2026-10-16T06:00:00.000Z"); + + const recordOnly = await makeMedication( + user, + { + name: "Record only", + startsOn: new Date("2026-10-01"), + createdAt: created, + trackIntake: false, + }, + [daily("07:00", "08:00")], + ); + med.recordOnly = recordOnly.id; + await intake(recordOnly.id, "2026-10-16T05:00:00.000Z"); + + const asNeeded = await makeMedication( + user, + { name: "As needed", createdAt: created, asNeeded: true }, + [], + ); + med.asNeeded = asNeeded.id; + + const booster = await makeMedication( + user, + { name: "Booster", createdAt: created }, + [daily("09:00", "10:00")], + ); + med.booster = booster.id; + + const course = await makeMedication( + user, + { + name: "Course tablet", + startsOn: new Date("2026-10-01"), + createdAt: new Date("2026-07-25T06:00:00.000Z"), + }, + [daily("08:00", "09:00")], + ); + med.course = course.id; + await prisma.medicationCourse.createMany({ + data: [ + { + userId: ownerId, + medicationId: course.id, + startsOn: new Date("2026-08-01"), + endsOn: new Date("2026-08-31"), + }, + { + userId: ownerId, + medicationId: course.id, + startsOn: new Date("2026-10-01"), + endsOn: null, + }, + ], + }); + + const doomed = await makeMedication( + user, + { name: "To delete", createdAt: created }, + [daily("12:00", "13:00")], + ); + med.doomed = doomed.id; + await prisma.medicationIngredient.create({ + data: { + userId: ownerId, + medicationId: doomed.id, + nutrient: "magnesium", + amountPerUnit: 100, + unit: "mg", + source: "MANUAL", + }, + }); + await makeToken(user, { + name: "kit-ingest", + permissions: [`medication:${doomed.id}:ingest`], + }); + + const stranger = await makeUser({ timezone: "UTC" }); + const foreign = await makeMedication(stranger, { name: "Not yours" }); + med.foreign = foreign.id; + + const w = await delegateOf(prisma, user, "WRITE"); + const r = await delegateOf(prisma, user, "READ"); + const m = await delegateOf(prisma, user, "MANAGE"); + writer = { auth: w.auth, headers: w.headers }; + reader = { auth: r.auth, headers: r.headers }; + manager = { auth: m.auth, headers: m.headers }; + userIds = [ownerId, w.delegate.id, r.delegate.id, m.delegate.id]; +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("GET /api/medications/{id} parity", () => { + it("01 a rolling injection with stock, a composition and a custom category", async () => { + await get("01-rolling", med.rolling); + }); + + it("02 a daily tablet without stock", async () => { + await get("02-daily", med.tablet); + }); + + it("03 a record-only medication", async () => { + await get("03-record-only", med.recordOnly); + }); + + it("04 another account's medication", async () => { + await get("04-not-found", med.foreign); + }); + + it("05 a reading delegate, and nobody", async () => { + await get("05a-reader", med.rolling, reader); + await get("05b-anonymous", med.rolling, { auth: anonymous }); + }); +}); + +describe("PUT /api/medications/{id} parity", () => { + it("11 plain fields", async () => { + await put("11-plain-fields", med.tablet, { + name: "Daily tablet renamed", + dose: "20mg", + treatmentClass: "GENERIC", + dosesPerUnit: 30, + reorderLeadDays: 5, + deliveryForm: "ORAL", + notificationsEnabled: false, + liveActivityEnabled: true, + criticalAlarmEnabled: true, + atcCode: "C09AA05", + rxNormCode: null, + }); + }); + + it("12 a schedule replace whose dose time leaves the stale window", async () => { + await put("12-schedule-replace", med.tablet, { + schedules: [ + { + windowStart: "08:00", + windowEnd: "10:00", + timesOfDay: ["11:30"], + label: "Late morning", + dose: "10mg", + unitsPerDose: 2, + reminderGraceMinutes: 30, + }, + { + windowStart: "20:00", + windowEnd: "22:00", + daysOfWeek: [1, 3, 5], + intervalWeeks: 2, + doseWindows: [{ timeOfDay: "20:00", start: "19:30", end: "21:00" }], + }, + ], + }); + }); + + it("13 the same schedule echoed back", async () => { + await put("13-schedule-echo", med.tablet, { + schedules: [ + { + windowStart: "11:30", + windowEnd: "11:30", + timesOfDay: ["11:30"], + label: "Late morning", + dose: "10mg", + unitsPerDose: 2, + reminderGraceMinutes: 30, + rrule: "FREQ=DAILY", + }, + { + windowStart: "20:00", + windowEnd: "22:00", + daysOfWeek: [1, 3, 5], + intervalWeeks: 2, + doseWindows: [{ timeOfDay: "20:00", start: "19:30", end: "21:00" }], + }, + ], + }); + }); + + it("14 the top-level reminder window", async () => { + await put("14-grace-bridge", med.tablet, { reminderGraceMinutes: 45 }); + }); + + it("15 a pause and a resume", async () => { + await put("15a-pause", med.tablet, { active: false }); + await put("15b-resume", med.tablet, { active: true }); + }); + + it("16 automatic recording switched on", async () => { + await put("16-auto-mark-taken", med.tablet, { autoMarkTaken: true }); + }); + + it("17 the category writes", async () => { + await put("17a-custom-category", med.tablet, { category: CUSTOM_KEY }); + await put( + "17b-native-other-kept", + med.tablet, + { category: "OTHER" }, + { auth: ownerBearer }, + ); + await put("17c-web-other", med.tablet, { category: "OTHER" }); + await put("17d-unknown-category", med.tablet, { + category: "custom:0a6c2d4e-1111-4222-8333-944455556666", + }); + }); + + it("18 a record-only medication and both tracking transitions", async () => { + await put("18a-record-schedule-kept", med.recordOnly, { + name: "Record only renamed", + schedules: [daily("06:00", "07:00")], + oneShot: false, + }); + await put("18b-tracking-on", med.recordOnly, { trackIntake: true }); + await put("18c-tracking-off", med.recordOnly, { trackIntake: false }); + }); + + it("19 the as-needed flips and their refusals", async () => { + await put("19a-scheduled-without-schedules", med.asNeeded, { + asNeeded: false, + }); + await put("19b-scheduled", med.asNeeded, { + asNeeded: false, + schedules: [daily("08:00", "09:00")], + }); + await put("19c-as-needed-keeping-schedules", med.asNeeded, { + asNeeded: true, + }); + await put("19d-as-needed", med.asNeeded, { + asNeeded: true, + schedules: [], + }); + }); + + it("20 the one-shot refusals and a conversion", async () => { + await put("20a-one-shot-two-schedules", med.booster, { + oneShot: true, + startsOn: "2026-10-20", + schedules: [daily("09:00", "10:00"), daily("18:00", "19:00")], + }); + await put("20b-one-shot-recurrence", med.booster, { + oneShot: true, + startsOn: "2026-10-20", + schedules: [ + { windowStart: "09:00", windowEnd: "10:00", rrule: "FREQ=DAILY" }, + ], + }); + await put("20c-one-shot", med.booster, { + oneShot: true, + startsOn: "2026-10-20", + schedules: [{ windowStart: "09:00", windowEnd: "10:00" }], + }); + }); + + it("21 the course refusals and a course edit", async () => { + await put("21a-course-overlap", med.course, { startsOn: "2026-08-20" }); + await put("21b-course-window-required", med.course, { + startsOn: null, + endsOn: null, + }); + await put("21c-course-end", med.course, { endsOn: "2026-12-31" }); + }); + + it("22 a units change on a medication with a composition", async () => { + await put("22-composition-units", med.rolling, { unitsPerDose: 2 }); + }); + + it("23 a body the schema refuses, and another account's medication", async () => { + await put("23a-invalid", med.tablet, { + name: "", + unitsPerDose: -1, + schedules: "daily", + }); + await put("23b-not-found", med.foreign, { name: "Taken over" }); + }); + + it("24 a delegate who may write, one who may manage, and nobody", async () => { + await put("24a-writer", med.tablet, { name: "By a writer" }, writer); + await put("24b-manager", med.tablet, { name: "By a manager" }, manager); + await put( + "24c-anonymous", + med.tablet, + { name: "By nobody" }, + { auth: anonymous }, + ); + }); +}); + +describe("DELETE /api/medications/{id} parity", () => { + it("31 the owner deletes a medication with a composition and an ingest token", async () => { + await del("31-delete", med.doomed); + }); + + it("32 the same medication again", async () => { + await del("32-not-found", med.doomed); + }); + + it("33 a managing delegate, and nobody", async () => { + await del("33a-manager", med.booster, manager); + await del("33b-anonymous", med.booster, { auth: anonymous }); + }); +}); diff --git a/tests/integration/routes/parity/medication-intake-bulk.parity.test.ts b/tests/integration/routes/parity/medication-intake-bulk.parity.test.ts new file mode 100644 index 000000000..ec3fc649b --- /dev/null +++ b/tests/integration/routes/parity/medication-intake-bulk.parity.test.ts @@ -0,0 +1,450 @@ +/** + * Parity for `POST /api/medications/intake/bulk` while the batch ingest moves + * into `src/lib/medications/`. + * + * One record walks through the cases the route tells apart, in order, each + * request seeing what the earlier ones stored. A first mixed batch carries + * one entry per per-entry outcome: an unknown medication, a record-only one, + * an unstable idempotency key and an unstable external id, a take inside its + * band, a pinned take and both pin refusals, a skip on a named slot, a + * pending echo onto a recorded dose and one onto a fresh slot, a standalone + * take with a site, a dose override and a key, its replay, a disallowed + * site, an Apple dose and its repeat inside the batch, an Apple dose whose + * slot another dose holds, and a skip that converges onto the standalone + * dose. Then the same batch again, a background and a backfill sync, the + * refusals (too large, a body the schema refuses, an Apple entry on a native + * medication), a native client, a narrow token, a delegate and nobody. + * + * After each request the snapshot holds the response, the wide event, the + * recorded tails, every medication row of the accounts, and the sync + * checkpoint and Apple Health arrival rows the batch writes. The clock is + * pinned (`Date` only), so the snapshot is the same on every day it runs. + */ +import { afterAll, beforeAll, describe, it, vi } from "vitest"; + +import { POST } from "@/app/api/medications/intake/bulk/route"; + +import { anonymous, type RouteAuth } from "../../../kit/call-route"; +import { + makeMedication, + makeSession, + makeToken, + makeUser, +} from "../../../kit/factories"; +import { getPrismaClient, truncateAllTables } from "../../setup"; +import { delegateOf, parityStep } from "./medication-state"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); + +// The tails the route starts without awaiting them (the cross-device push, +// the web-push dose clear with its badge count), recorded instead of run +// (see `effectRecorder`). +vi.mock( + "@/lib/notifications/medication-intake-sync", + async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/notifications/medication-intake-sync") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + queueMedicationIntakeSync: (...args: unknown[]) => { + effectRecorder.record("queueMedicationIntakeSync", args); + }, + }; + }, +); +vi.mock("@/lib/notifications/web-push-clear", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + dispatchMedicationIntakeWebClearBulk: async (...args: unknown[]) => { + effectRecorder.record("dispatchMedicationIntakeWebClearBulk", args); + }, + }; +}); +vi.mock("@/lib/medications/outstanding-doses", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/medications/outstanding-doses") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + countOutstandingDosesToday: async (...args: unknown[]) => { + effectRecorder.record("countOutstandingDosesToday", args); + return 2; + }, + }; +}); + +const prisma = getPrismaClient(); +const NOW = new Date("2026-10-15T12:00:00.000Z"); +const ROUTE = "/api/medications/intake/bulk"; +const INVALID_ACTION = "medications.intake.bulk.validation-failed"; + +let ownerId: string; +let userIds: string[]; +let owner: RouteAuth; +let ownerBearer: RouteAuth; +let narrowBearer: RouteAuth; +let writer: { auth: RouteAuth; headers: Record }; +const med: Record = {}; + +type Caller = { auth?: RouteAuth; headers?: Record }; + +/** The sync checkpoint and the Apple Health arrival rows of the owner. */ +async function syncState() { + const [user, arrivals] = await Promise.all([ + prisma.user.findUniqueOrThrow({ + where: { id: ownerId }, + select: { lastSyncedAt: true }, + }), + prisma.healthKitTypeSync.findMany({ + where: { userId: ownerId }, + orderBy: { type: "asc" }, + select: { + type: true, + lastReceivedAt: true, + lastTrigger: true, + lastNewSampleAt: true, + lastBackgroundAt: true, + lastSeenDuplicateAt: true, + }, + }), + ]); + return { lastSyncedAt: user.lastSyncedAt, arrivals }; +} + +/** + * An audit row a refusal writes without awaiting it: the schema refusal's + * own, and the narrow token's `auth.bearer.failure`. + */ +async function auditRows(action: string, count: number): Promise { + // Polled on the real timer: the pinned clock stamps the rows read back, + // and nothing here may move it. + for (let attempt = 0; attempt < 250; attempt += 1) { + const rows = await prisma.auditLog.count({ where: { action } }); + if (rows >= count) return; + await new Promise((resolve) => setTimeout(resolve, 20)); + } + throw new Error(`fewer than ${count} ${action} rows landed`); +} + +async function post( + name: string, + body: unknown, + options: Caller & { settle?: () => Promise } = {}, +) { + await parityStep({ + name: `medication-intake-bulk.${name}`, + handler: POST, + options: { + method: "POST", + route: ROUTE, + body, + auth: options.auth ?? owner, + headers: options.headers, + }, + userIds, + now: NOW, + settle: options.settle, + extra: syncState, + }); +} + +async function intake( + medicationId: string, + data: { + scheduledFor: string; + takenAt?: string; + source?: "REMINDER" | "APPLE_HEALTH"; + externalId?: string; + }, +) { + await prisma.medicationIntakeEvent.create({ + data: { + userId: ownerId, + medicationId, + scheduledFor: new Date(data.scheduledFor), + takenAt: data.takenAt ? new Date(data.takenAt) : null, + source: data.source ?? "REMINDER", + externalId: data.externalId ?? null, + createdAt: new Date("2026-10-01T06:00:00.000Z"), + }, + }); +} + +const daily = (time: string, windowEnd: string) => ({ + windowStart: time, + windowEnd, + timesOfDay: [time], + rrule: "FREQ=DAILY", +}); + +/** One entry per per-entry outcome the loop tells apart. */ +function mixedBatch() { + return { + syncTrigger: "foreground", + entries: [ + { medicationId: med.foreign, takenAt: "2026-10-15T08:10:00.000Z" }, + { medicationId: med.recordOnly, takenAt: "2026-10-15T07:10:00.000Z" }, + { + medicationId: med.daily, + takenAt: "2026-10-15T08:20:00.000Z", + idempotencyKey: "", + }, + { medicationId: med.daily, takenAt: "2026-10-15T08:30:00.000Z" }, + { + medicationId: med.daily, + takenAt: "2026-10-13T11:00:00.000Z", + forceSlotInstant: "2026-10-13T08:00:00.000Z", + }, + { + medicationId: med.daily, + takenAt: "2026-10-13T16:00:00.000Z", + forceSlotInstant: "2026-10-13T15:00:00.000Z", + }, + { + medicationId: med.daily, + takenAt: "2026-10-14T23:00:00.000Z", + forceSlotInstant: "2026-10-14T20:00:00.000Z", + }, + { + medicationId: med.daily, + skipped: true, + scheduledFor: "2026-10-15T20:00:00.000Z", + }, + { medicationId: med.daily, scheduledFor: "2026-10-14T20:00:00.000Z" }, + { medicationId: med.daily, scheduledFor: "2026-10-16T08:00:00.000Z" }, + { + medicationId: med.pen, + takenAt: "2026-10-15T10:00:00.000Z", + injectionSite: "ABDOMEN_LEFT", + doseTaken: "0.5 pen", + idempotencyKey: "bulk-key-0001", + }, + { + medicationId: med.pen, + takenAt: "2026-10-15T10:00:00.000Z", + idempotencyKey: "bulk-key-0001", + }, + { + medicationId: med.pen, + takenAt: "2026-10-15T10:30:00.000Z", + injectionSite: "ABDOMEN_RIGHT", + }, + { + medicationId: med.apple, + takenAt: "2026-10-15T09:20:00.000Z", + source: "APPLE_HEALTH", + externalId: "hk-dose-0001", + }, + { + medicationId: med.apple, + takenAt: "2026-10-15T09:20:00.000Z", + source: "APPLE_HEALTH", + externalId: "hk-dose-0001", + }, + { + medicationId: med.apple, + takenAt: "2026-10-14T09:30:00.000Z", + source: "APPLE_HEALTH", + externalId: "hk-dose-0002", + }, + { + medicationId: med.apple, + takenAt: "2026-10-14T10:00:00.000Z", + source: "APPLE_HEALTH", + externalId: "", + }, + { + medicationId: med.pen, + skipped: true, + scheduledFor: "2026-10-15T10:00:00.000Z", + }, + ], + }; +} + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const user = await makeUser({ + timezone: "UTC", + globalExcludedInjectionSites: ["ABDOMEN_RIGHT"], + }); + ownerId = user.id; + owner = await makeSession(user); + ownerBearer = await makeToken(user, { name: "kit-phone" }); + narrowBearer = await makeToken(user, { + name: "kit-ingest", + permissions: ["medication:ingest"], + }); + const created = new Date("2026-10-01T06:00:00.000Z"); + + const dailyMed = await makeMedication( + user, + { + name: "Twice daily", + startsOn: new Date("2026-10-01"), + createdAt: created, + snoozedUntil: new Date("2026-10-15T12:30:00.000Z"), + }, + [daily("08:00", "10:00"), daily("20:00", "22:00")], + ); + med.daily = dailyMed.id; + await prisma.medicationInventoryItem.create({ + data: { + userId: ownerId, + medicationId: dailyMed.id, + unitsTotal: 30, + unitsRemaining: 30, + purchasedAt: created, + }, + }); + await intake(dailyMed.id, { scheduledFor: "2026-10-15T08:00:00.000Z" }); + await intake(dailyMed.id, { + scheduledFor: "2026-10-14T20:00:00.000Z", + takenAt: "2026-10-14T20:05:00.000Z", + }); + + med.pen = ( + await makeMedication( + user, + { + name: "As-needed pen", + createdAt: created, + asNeeded: true, + deliveryForm: "INJECTION", + trackInjectionSites: true, + allowedInjectionSites: ["ABDOMEN_LEFT", "ABDOMEN_RIGHT"], + }, + [], + ) + ).id; + + const apple = await makeMedication( + user, + { + name: "Apple mirror", + startsOn: new Date("2026-10-01"), + createdAt: created, + externalSource: "APPLE_HEALTH", + externalId: "hk-concept-0001", + }, + [daily("09:00", "11:00")], + ); + med.apple = apple.id; + await intake(apple.id, { + scheduledFor: "2026-10-14T09:00:00.000Z", + takenAt: "2026-10-14T09:05:00.000Z", + source: "APPLE_HEALTH", + externalId: "hk-dose-0000", + }); + + med.recordOnly = ( + await makeMedication( + user, + { name: "Record only", createdAt: created, trackIntake: false }, + [daily("07:00", "08:00")], + ) + ).id; + + const stranger = await makeUser({ timezone: "UTC" }); + med.foreign = (await makeMedication(stranger, { name: "Not yours" })).id; + + const w = await delegateOf(prisma, user, "WRITE"); + writer = { auth: w.auth, headers: w.headers }; + userIds = [ownerId, w.delegate.id]; +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("POST /api/medications/intake/bulk parity", () => { + it("01 a mixed first batch", async () => { + await post("01-mixed", mixedBatch()); + }); + + it("02 the same batch again", async () => { + await post("02-mixed-again", mixedBatch()); + }); + + it("03 a background sync and a backfill", async () => { + await post("03a-background", { + syncTrigger: "background", + entries: [ + { + medicationId: med.apple, + takenAt: "2026-10-13T09:10:00.000Z", + source: "APPLE_HEALTH", + externalId: "hk-dose-0003", + }, + ], + }); + await post("03b-backfill", { + syncTrigger: "backfill", + entries: [ + { + medicationId: med.apple, + takenAt: "2026-10-12T09:10:00.000Z", + source: "APPLE_HEALTH", + externalId: "hk-dose-0004", + }, + ], + }); + }); + + it("04 the batch refusals", async () => { + await post("04a-too-large", { + entries: Array.from({ length: 501 }, () => ({ + medicationId: med.daily, + })), + }); + await post( + "04b-invalid", + { entries: [{ medicationId: "", takenAt: "never" }] }, + { settle: () => auditRows(INVALID_ACTION, 1) }, + ); + await post("04c-apple-not-mirrored", { + entries: [ + { + medicationId: med.daily, + takenAt: "2026-10-12T08:10:00.000Z", + source: "APPLE_HEALTH", + externalId: "hk-dose-0005", + }, + ], + }); + }); + + it("05 a native client, a narrow token, a delegate and nobody", async () => { + const batch = { + entries: [ + { medicationId: med.daily, takenAt: "2026-10-12T08:10:00.000Z" }, + ], + }; + await post("05a-native", batch, { auth: ownerBearer }); + await post("05b-narrow-token", batch, { + auth: narrowBearer, + settle: () => auditRows("auth.bearer.failure", 1), + }); + await post("05c-delegate", batch, writer); + await post("05d-anonymous", batch, { auth: anonymous }); + }); +}); diff --git a/tests/integration/routes/parity/medication-intake-status.parity.test.ts b/tests/integration/routes/parity/medication-intake-status.parity.test.ts new file mode 100644 index 000000000..b83135251 --- /dev/null +++ b/tests/integration/routes/parity/medication-intake-status.parity.test.ts @@ -0,0 +1,477 @@ +/** + * Parity for `GET` and `POST /api/medications/intake` while the today list, + * the compliance buckets and the status update move into + * `src/lib/medications/`. + * + * One record walks through the cases the route tells apart, in order, each + * request seeing what the earlier ones stored. The reads: today's list (with + * the projection it runs, and every status a row can read as), the + * compliance buckets cold and from the cache, a query the schema refuses, a + * reading delegate and nobody. The updates: a take in place, a take that + * re-binds to another slot (tombstone and converge, the recorded site + * carried), a pinned take and a pin that is no slot, a skip over a recorded + * dose, a snooze with and without its end, the not-found answers (an unknown + * event, another account's, a tombstoned one), the record-only rule both + * ways, the injection-site refusal and an allowed site, a body the schema + * refuses, and a delegate who marks an open dose, is refused a snooze and a + * correction, a reader and nobody. + * + * After each request the snapshot holds the response, the wide event, the + * recorded tails and every medication row of the accounts. The clock is + * pinned (`Date` only), so the snapshot is the same on every day it runs. + */ +import { afterAll, beforeAll, describe, it, vi } from "vitest"; + +import { GET, POST } from "@/app/api/medications/intake/route"; + +import { anonymous, type RouteAuth } from "../../../kit/call-route"; +import { makeMedication, makeSession, makeUser } from "../../../kit/factories"; +import { getPrismaClient, truncateAllTables } from "../../setup"; +import { delegateOf, parityStep } from "./medication-state"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); + +// The tails the route starts without awaiting them (the cross-device push, +// the web-push dose clear with its badge count, the owner's notification of a +// delegated dose), recorded instead of run (see `effectRecorder`). +vi.mock( + "@/lib/notifications/medication-intake-sync", + async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/notifications/medication-intake-sync") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + queueMedicationIntakeSync: (...args: unknown[]) => { + effectRecorder.record("queueMedicationIntakeSync", args); + }, + }; + }, +); +vi.mock("@/lib/notifications/web-push-clear", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + dispatchMedicationIntakeWebClear: async (...args: unknown[]) => { + effectRecorder.record("dispatchMedicationIntakeWebClear", args); + }, + }; +}); +vi.mock("@/lib/medications/outstanding-doses", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/medications/outstanding-doses") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + countOutstandingDosesToday: async (...args: unknown[]) => { + effectRecorder.record("countOutstandingDosesToday", args); + return 2; + }, + }; +}); +vi.mock("@/lib/notifications/delegated-intake", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/notifications/delegated-intake") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + notifyDelegatedIntake: async (...args: unknown[]) => { + effectRecorder.record("notifyDelegatedIntake", args); + }, + }; +}); + +const prisma = getPrismaClient(); +const NOW = new Date("2026-10-15T12:00:00.000Z"); +const ROUTE = "/api/medications/intake"; + +let ownerId: string; +let userIds: string[]; +let owner: RouteAuth; +let writer: { auth: RouteAuth; headers: Record }; +let reader: { auth: RouteAuth; headers: Record }; +const ev: Record = {}; + +type Caller = { auth?: RouteAuth; headers?: Record }; + +async function get( + name: string, + query: Record, + caller: Caller = {}, +) { + await parityStep({ + name: `medication-intake-status.${name}`, + handler: GET, + options: { + method: "GET", + route: ROUTE, + query, + auth: caller.auth ?? owner, + headers: caller.headers, + }, + userIds, + now: NOW, + }); +} + +/** + * A take that re-binds to another slot answers with the row the shared slot + * write returns, which carries fewer fields than the registered event + * resource. The parity snapshot pins that answer as it is; the contract check + * is the route's own tests' business. + */ +const SLOT_MOVE_ANSWER = { + skip: "a slot move answers with the narrower slot-write row", +}; + +async function post( + name: string, + body: unknown, + caller: Caller & { contract?: { skip: string } } = {}, +) { + await parityStep({ + name: `medication-intake-status.${name}`, + handler: POST, + options: { + method: "POST", + route: ROUTE, + body, + auth: caller.auth ?? owner, + headers: caller.headers, + contract: caller.contract, + }, + userIds, + now: NOW, + }); +} + +async function intake( + key: string, + medicationId: string, + data: { + scheduledFor: string; + takenAt?: string; + skipped?: boolean; + autoMissed?: boolean; + deletedAt?: string; + injectionSite?: "ABDOMEN_LEFT"; + userId?: string; + }, +) { + const row = await prisma.medicationIntakeEvent.create({ + data: { + userId: data.userId ?? ownerId, + medicationId, + scheduledFor: new Date(data.scheduledFor), + takenAt: data.takenAt ? new Date(data.takenAt) : null, + skipped: data.skipped ?? false, + autoMissed: data.autoMissed ?? false, + deletedAt: data.deletedAt ? new Date(data.deletedAt) : null, + injectionSite: data.injectionSite ?? null, + source: "REMINDER", + createdAt: new Date("2026-10-01T06:00:00.000Z"), + }, + }); + ev[key] = row.id; +} + +const daily = (time: string, windowEnd: string) => ({ + windowStart: time, + windowEnd, + timesOfDay: [time], + rrule: "FREQ=DAILY", +}); + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const user = await makeUser({ timezone: "UTC" }); + ownerId = user.id; + owner = await makeSession(user); + const created = new Date("2026-10-01T06:00:00.000Z"); + + const twice = await makeMedication( + user, + { + name: "Twice daily", + startsOn: new Date("2026-10-01"), + createdAt: created, + snoozedUntil: new Date("2026-10-15T12:30:00.000Z"), + deliveryForm: "INJECTION", + trackInjectionSites: true, + allowedInjectionSites: ["ABDOMEN_LEFT"], + }, + [daily("08:00", "10:00"), daily("20:00", "22:00")], + ); + await prisma.medicationInventoryItem.create({ + data: { + userId: ownerId, + medicationId: twice.id, + unitsTotal: 30, + unitsRemaining: 30, + purchasedAt: created, + }, + }); + await intake("todayMorning", twice.id, { + scheduledFor: "2026-10-15T08:00:00.000Z", + }); + await intake("todayEvening", twice.id, { + scheduledFor: "2026-10-15T20:00:00.000Z", + }); + await intake("moveMe", twice.id, { + scheduledFor: "2026-10-14T08:00:00.000Z", + injectionSite: "ABDOMEN_LEFT", + }); + await intake("pinMe", twice.id, { + scheduledFor: "2026-10-13T20:00:00.000Z", + }); + await intake("skipMe", twice.id, { + scheduledFor: "2026-10-12T08:00:00.000Z", + takenAt: "2026-10-12T08:10:00.000Z", + }); + await intake("snoozeMe", twice.id, { + scheduledFor: "2026-10-11T08:00:00.000Z", + }); + await intake("resolved", twice.id, { + scheduledFor: "2026-10-10T08:00:00.000Z", + takenAt: "2026-10-10T08:05:00.000Z", + }); + await intake("tombstoned", twice.id, { + scheduledFor: "2026-10-09T08:00:00.000Z", + deletedAt: "2026-10-09T09:00:00.000Z", + }); + + const pen = await makeMedication( + user, + { + name: "Daily pen", + startsOn: new Date("2026-10-01"), + createdAt: created, + deliveryForm: "INJECTION", + trackInjectionSites: true, + allowedInjectionSites: ["ABDOMEN_LEFT", "ABDOMEN_RIGHT"], + }, + [daily("09:00", "11:00")], + ); + await intake("penToday", pen.id, { + scheduledFor: "2026-10-15T09:00:00.000Z", + }); + await prisma.user.update({ + where: { id: ownerId }, + data: { globalExcludedInjectionSites: ["ABDOMEN_RIGHT"] }, + }); + + const mixed = await makeMedication( + user, + { + name: "Three times", + startsOn: new Date("2026-10-01"), + createdAt: created, + }, + [daily("06:00", "07:00"), daily("10:00", "11:00"), daily("13:00", "14:00")], + ); + await intake("missed", mixed.id, { + scheduledFor: "2026-10-15T06:00:00.000Z", + autoMissed: true, + }); + await intake("taken", mixed.id, { + scheduledFor: "2026-10-15T10:00:00.000Z", + takenAt: "2026-10-15T10:05:00.000Z", + }); + await intake("skipped", mixed.id, { + scheduledFor: "2026-10-15T13:00:00.000Z", + skipped: true, + }); + + const recordOnly = await makeMedication( + user, + { name: "Record only", createdAt: created, trackIntake: false }, + [daily("07:00", "08:00")], + ); + await intake("recordOpen", recordOnly.id, { + scheduledFor: "2026-10-14T07:00:00.000Z", + }); + await intake("recordTaken", recordOnly.id, { + scheduledFor: "2026-10-13T07:00:00.000Z", + takenAt: "2026-10-13T07:05:00.000Z", + }); + + const stranger = await makeUser({ timezone: "UTC" }); + const foreign = await makeMedication(stranger, { name: "Not yours" }); + await intake("foreign", foreign.id, { + scheduledFor: "2026-10-15T08:00:00.000Z", + userId: stranger.id, + }); + + const w = await delegateOf(prisma, user, "WRITE"); + const r = await delegateOf(prisma, user, "READ"); + writer = { auth: w.auth, headers: w.headers }; + reader = { auth: r.auth, headers: r.headers }; + userIds = [ownerId, w.delegate.id, r.delegate.id]; +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("GET /api/medications/intake parity", () => { + it("01 today's list", async () => { + await get("01-today", { scope: "today" }); + }); + + it("02 the compliance buckets, cold and from the cache", async () => { + await get("02a-compliance-cold", { scope: "compliance", days: "7" }); + await get("02b-compliance-warm", { scope: "compliance", days: "7" }); + }); + + it("03 a query the schema refuses", async () => { + await get("03-invalid", { scope: "yesterday", days: "0" }); + }); + + it("04 a reading delegate, and nobody", async () => { + await get("04a-reader", { scope: "today" }, reader); + await get("04b-anonymous", { scope: "today" }, { auth: anonymous }); + }); +}); + +describe("POST /api/medications/intake parity", () => { + it("11 a take in place", async () => { + await post("11-take-in-place", { + intakeId: ev.todayMorning, + status: "taken", + takenAt: "2026-10-15T08:30:00.000Z", + }); + }); + + it("12 a take that re-binds to another slot", async () => { + await post( + "12-take-slot-move", + { + intakeId: ev.moveMe, + status: "taken", + takenAt: "2026-10-14T20:30:00.000Z", + }, + { contract: SLOT_MOVE_ANSWER }, + ); + }); + + it("13 a pinned take, and a pin that is no slot", async () => { + await post( + "13a-pin", + { + intakeId: ev.pinMe, + status: "taken", + takenAt: "2026-10-13T11:00:00.000Z", + forceSlotInstant: "2026-10-13T08:00:00.000Z", + }, + { contract: SLOT_MOVE_ANSWER }, + ); + await post("13b-pin-not-a-slot", { + intakeId: ev.snoozeMe, + status: "taken", + forceSlotInstant: "2026-10-11T15:00:00.000Z", + }); + }); + + it("14 a skip over a recorded dose", async () => { + await post("14-skip", { intakeId: ev.skipMe, status: "skipped" }); + }); + + it("15 a snooze with its end, and one without", async () => { + await post("15a-snooze-until", { + intakeId: ev.snoozeMe, + status: "snoozed", + snoozedUntil: "2026-10-15T13:00:00.000Z", + }); + await post("15b-snooze-default", { + intakeId: ev.snoozeMe, + status: "snoozed", + }); + }); + + it("16 the not-found answers", async () => { + await post("16a-unknown", { + intakeId: "cmv0000000000000000000000", + status: "taken", + }); + await post("16b-foreign", { intakeId: ev.foreign, status: "taken" }); + await post("16c-tombstoned", { intakeId: ev.tombstoned, status: "taken" }); + }); + + it("17 the record-only rule both ways", async () => { + await post("17a-record-only-open", { + intakeId: ev.recordOpen, + status: "taken", + }); + await post("17b-record-only-correction", { + intakeId: ev.recordTaken, + status: "taken", + takenAt: "2026-10-13T07:20:00.000Z", + }); + }); + + it("18 the injection-site refusal, and an allowed site", async () => { + await post("18a-site-not-allowed", { + intakeId: ev.penToday, + status: "taken", + takenAt: "2026-10-15T09:15:00.000Z", + injectionSite: "ABDOMEN_RIGHT", + }); + await post("18b-site", { + intakeId: ev.penToday, + status: "taken", + takenAt: "2026-10-15T09:15:00.000Z", + injectionSite: "ABDOMEN_LEFT", + }); + }); + + it("19 a body the schema refuses", async () => { + await post("19-invalid", { intakeId: "", status: "forgotten" }); + }); + + it("20 a delegate who marks an open dose, a snooze, a correction, a reader and nobody", async () => { + await post( + "20a-writer-take", + { intakeId: ev.todayEvening, status: "taken" }, + { ...writer, contract: SLOT_MOVE_ANSWER }, + ); + await post( + "20b-writer-snooze-refused", + { intakeId: ev.snoozeMe, status: "snoozed" }, + writer, + ); + await post( + "20c-writer-correction-refused", + { intakeId: ev.resolved, status: "skipped" }, + writer, + ); + await post( + "20d-reader", + { intakeId: ev.snoozeMe, status: "taken" }, + reader, + ); + await post( + "20e-anonymous", + { intakeId: ev.snoozeMe, status: "taken" }, + { auth: anonymous }, + ); + }); +}); diff --git a/tests/integration/routes/parity/medication-intake.parity.test.ts b/tests/integration/routes/parity/medication-intake.parity.test.ts new file mode 100644 index 000000000..5d66bd899 --- /dev/null +++ b/tests/integration/routes/parity/medication-intake.parity.test.ts @@ -0,0 +1,541 @@ +/** + * Parity for `POST` and `GET /api/medications/{id}/intake` while the dose + * write and the history list move into `src/lib/medications/`. + * + * One record walks through the cases the route tells apart, in order, each + * request seeing what the earlier ones stored. The writes: a take inside a + * band that converges onto the pending reminder row, an as-needed take + * recorded standalone and its repeat inside the dedup window, a skip on a + * named slot, a slot-less skip, a skip onto the standalone dose by a + * delegate (refused) and by the owner, a pinned take and the two pin refusals, a + * backfill onto a named earlier slot, a body idempotency key and its replay, + * an Idempotency-Key header and its replay, a native take, the owner's skip + * over a recorded dose and a delegate's refused one, a delegate's take on an + * open slot, a reading delegate and nobody, the injection-site refusal and a + * site with a dose override, a one-shot dose, a body the schema refuses, and + * the not-found and record-only answers. The reads: the default list, a + * completed page sorted by take time, the skips, a query the schema refuses, + * the not-found answer, a reading delegate and nobody. + * + * After each request the snapshot holds the response, the wide event, the + * recorded tails and every medication row of the accounts. The clock is + * pinned (`Date` only), so the snapshot is the same on every day it runs. + */ +import { afterAll, beforeAll, describe, it, vi } from "vitest"; + +import { GET, POST } from "@/app/api/medications/[id]/intake/route"; + +import { anonymous, type RouteAuth } from "../../../kit/call-route"; +import { + makeMedication, + makeSession, + makeToken, + makeUser, +} from "../../../kit/factories"; +import { getPrismaClient, truncateAllTables } from "../../setup"; +import { delegateOf, parityStep } from "./medication-state"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); + +// The tails the route starts without awaiting them (the cross-device push, +// the web-push dose clear with its badge count, the owner's notification of a +// delegated dose), recorded instead of run (see `effectRecorder`). +vi.mock( + "@/lib/notifications/medication-intake-sync", + async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/notifications/medication-intake-sync") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + queueMedicationIntakeSync: (...args: unknown[]) => { + effectRecorder.record("queueMedicationIntakeSync", args); + }, + }; + }, +); +vi.mock("@/lib/notifications/web-push-clear", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + dispatchMedicationIntakeWebClear: async (...args: unknown[]) => { + effectRecorder.record("dispatchMedicationIntakeWebClear", args); + }, + }; +}); +vi.mock("@/lib/medications/outstanding-doses", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/medications/outstanding-doses") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + countOutstandingDosesToday: async (...args: unknown[]) => { + effectRecorder.record("countOutstandingDosesToday", args); + return 2; + }, + }; +}); +vi.mock("@/lib/notifications/delegated-intake", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/notifications/delegated-intake") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + notifyDelegatedIntake: async (...args: unknown[]) => { + effectRecorder.record("notifyDelegatedIntake", args); + }, + }; +}); + +const prisma = getPrismaClient(); +const NOW = new Date("2026-10-15T12:00:00.000Z"); +const ROUTE = "/api/medications/{id}/intake"; +/** + * A dose that converges onto a slot answers with the row the shared slot + * write returns, which carries fewer fields than the registered event + * resource. The parity snapshot pins that answer as it is; the contract check + * is the route's own tests' business. + */ +const DOSE_ANSWER = { + skip: "a slot write answers with the narrower slot-write row", +}; +const REFUSED = "medications.intake.update.refused"; + +let ownerId: string; +let userIds: string[]; +let owner: RouteAuth; +let ownerBearer: RouteAuth; +let writer: { auth: RouteAuth; headers: Record }; +let reader: { auth: RouteAuth; headers: Record }; +const med: Record = {}; + +type Caller = { auth?: RouteAuth; headers?: Record }; + +/** The refusal's audit row is written without awaiting it. */ +async function refusalRows(count: number): Promise { + // Polled on the real timer: `vi.waitFor` would advance the pinned clock by + // its interval on every poll, and the clock stamps the rows read back. + for (let attempt = 0; attempt < 250; attempt += 1) { + const rows = await prisma.auditLog.count({ where: { action: REFUSED } }); + if (rows >= count) return; + await new Promise((resolve) => setTimeout(resolve, 20)); + } + throw new Error(`fewer than ${count} refusal rows landed`); +} + +async function post( + name: string, + body: unknown, + options: Caller & { + medicationId?: string; + settle?: () => Promise; + } = {}, +) { + await parityStep({ + name: `medication-intake.${name}`, + handler: POST, + options: { + method: "POST", + route: ROUTE, + params: { id: options.medicationId ?? med.daily }, + body, + auth: options.auth ?? owner, + headers: options.headers, + contract: DOSE_ANSWER, + }, + userIds, + now: NOW, + settle: options.settle, + rewrite: withoutIdsInPaths, + }); +} + +/** + * The idempotency cache keys its rows on the request path, which carries the + * medication's generated id, and the replay's wide event names that path. A + * path reads with the fixture's name instead. The row's fingerprint, a hash + * over the path and so over the generated id, is dropped, because no + * placeholder can make it the same on two runs. + */ +function withoutIdsInPaths(value: unknown): unknown { + if (typeof value === "string") { + let out = value; + for (const [label, id] of Object.entries(med)) { + if (out !== id) out = out.replace(id, `<${label}>`); + } + return out; + } + if (Array.isArray(value)) return value.map(withoutIdsInPaths); + if (value && typeof value === "object" && !(value instanceof Date)) { + const out: Record = {}; + for (const [k, v] of Object.entries(value)) { + if (k === "requestFingerprint") continue; + out[k] = withoutIdsInPaths(v); + } + return out; + } + return value; +} + +async function list( + name: string, + query: Record = {}, + options: Caller & { medicationId?: string } = {}, +) { + await parityStep({ + name: `medication-intake.${name}`, + handler: GET, + options: { + method: "GET", + route: ROUTE, + params: { id: options.medicationId ?? med.daily }, + query, + auth: options.auth ?? owner, + headers: options.headers, + }, + userIds, + now: NOW, + rewrite: withoutIdsInPaths, + }); +} + +async function intake( + medicationId: string, + data: { + scheduledFor: string; + takenAt?: string; + skipped?: boolean; + source?: "REMINDER" | "WEB"; + }, +) { + await prisma.medicationIntakeEvent.create({ + data: { + userId: ownerId, + medicationId, + scheduledFor: new Date(data.scheduledFor), + takenAt: data.takenAt ? new Date(data.takenAt) : null, + skipped: data.skipped ?? false, + source: data.source ?? "WEB", + createdAt: new Date("2026-10-01T06:00:00.000Z"), + }, + }); +} + +const daily = (time: string, windowEnd: string) => ({ + windowStart: time, + windowEnd, + timesOfDay: [time], + rrule: "FREQ=DAILY", +}); + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const user = await makeUser({ timezone: "UTC" }); + ownerId = user.id; + owner = await makeSession(user); + ownerBearer = await makeToken(user, { name: "kit-phone" }); + const created = new Date("2026-10-01T06:00:00.000Z"); + + const dailyMed = await makeMedication( + user, + { + name: "Twice daily", + startsOn: new Date("2026-10-01"), + createdAt: created, + unitsPerDose: 1, + snoozedUntil: new Date("2026-10-15T12:30:00.000Z"), + }, + [daily("08:00", "10:00"), daily("20:00", "22:00")], + ); + med.daily = dailyMed.id; + await prisma.medicationInventoryItem.create({ + data: { + userId: ownerId, + medicationId: dailyMed.id, + unitsTotal: 30, + unitsRemaining: 30, + purchasedAt: created, + }, + }); + await intake(dailyMed.id, { + scheduledFor: "2026-10-15T08:00:00.000Z", + source: "REMINDER", + }); + await intake(dailyMed.id, { + scheduledFor: "2026-10-14T20:00:00.000Z", + takenAt: "2026-10-14T20:05:00.000Z", + }); + await intake(dailyMed.id, { + scheduledFor: "2026-10-13T08:00:00.000Z", + source: "REMINDER", + }); + await intake(dailyMed.id, { + scheduledFor: "2026-10-12T20:00:00.000Z", + skipped: true, + }); + + const pen = await makeMedication( + user, + { + name: "Weekly pen", + startsOn: new Date("2026-10-01"), + createdAt: created, + deliveryForm: "INJECTION", + trackInjectionSites: true, + allowedInjectionSites: ["ABDOMEN_LEFT", "ABDOMEN_RIGHT"], + }, + [daily("09:00", "11:00")], + ); + med.pen = pen.id; + await prisma.user.update({ + where: { id: ownerId }, + data: { globalExcludedInjectionSites: ["ABDOMEN_RIGHT"] }, + }); + + const oneShot = await makeMedication( + user, + { + name: "Single shot", + startsOn: new Date("2026-10-15"), + endsOn: new Date("2026-10-15"), + oneShot: true, + createdAt: created, + }, + [{ windowStart: "09:00", windowEnd: "11:00", timesOfDay: ["09:00"] }], + ); + med.oneShot = oneShot.id; + + med.asNeeded = ( + await makeMedication( + user, + { name: "As needed", createdAt: created, asNeeded: true }, + [], + ) + ).id; + + const recordOnly = await makeMedication( + user, + { name: "Record only", createdAt: created, trackIntake: false }, + [daily("07:00", "08:00")], + ); + med.recordOnly = recordOnly.id; + + const stranger = await makeUser({ timezone: "UTC" }); + med.foreign = (await makeMedication(stranger, { name: "Not yours" })).id; + + const w = await delegateOf(prisma, user, "WRITE"); + const r = await delegateOf(prisma, user, "READ"); + writer = { auth: w.auth, headers: w.headers }; + reader = { auth: r.auth, headers: r.headers }; + userIds = [ownerId, w.delegate.id, r.delegate.id]; +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("POST /api/medications/{id}/intake parity", () => { + it("01 a take inside its band, onto the pending reminder row", async () => { + await post("01-take-in-band", { takenAt: "2026-10-15T08:30:00.000Z" }); + }); + + it("02 an as-needed take recorded standalone, and its repeat inside the dedup window", async () => { + await post( + "02a-take-standalone", + { takenAt: "2026-10-15T11:00:00.000Z" }, + { medicationId: med.asNeeded }, + ); + await post( + "02b-take-standalone-repeat", + { takenAt: "2026-10-15T11:00:00.000Z" }, + { medicationId: med.asNeeded }, + ); + }); + + it("03 a skip on a named slot, a slot-less skip, and a skip onto a standalone dose", async () => { + await post("03a-skip-named-slot", { + skipped: true, + scheduledFor: "2026-10-15T20:00:00.000Z", + }); + await post("03b-skip-slotless", { skipped: true }); + await post( + "03c-delegate-skip-standalone-refused", + { skipped: true, scheduledFor: "2026-10-15T11:00:00.000Z" }, + { ...writer, medicationId: med.asNeeded, settle: () => refusalRows(1) }, + ); + await post( + "03d-owner-skip-standalone", + { skipped: true, scheduledFor: "2026-10-15T11:00:00.000Z" }, + { medicationId: med.asNeeded }, + ); + }); + + it("04 a pinned take and the two pin refusals", async () => { + await post("04a-pin", { + takenAt: "2026-10-14T11:00:00.000Z", + forceSlotInstant: "2026-10-14T08:00:00.000Z", + }); + await post("04b-pin-not-a-slot", { + takenAt: "2026-10-14T16:00:00.000Z", + forceSlotInstant: "2026-10-14T15:00:00.000Z", + }); + await post("04c-pin-occupied", { + takenAt: "2026-10-14T23:00:00.000Z", + forceSlotInstant: "2026-10-14T20:00:00.000Z", + }); + }); + + it("05 a backfill onto a named earlier slot", async () => { + await post("05-named-backfill", { + takenAt: "2026-10-15T11:30:00.000Z", + scheduledFor: "2026-10-13T08:00:00.000Z", + }); + }); + + it("06 a body idempotency key and its replay", async () => { + const body = { + takenAt: "2026-10-13T20:30:00.000Z", + idempotencyKey: "dose-key-0001", + }; + await post("06a-body-key", body); + await post("06b-body-key-replay", body); + }); + + it("07 an Idempotency-Key header and its replay", async () => { + const headers = { + "idempotency-key": "4b7e1c9a-2d3f-4a5b-8c6d-7e8f9a0b1c2d", + }; + const body = { takenAt: "2026-10-12T08:15:00.000Z" }; + await post("07a-header-key", body, { headers }); + await post("07b-header-key-replay", body, { headers }); + }); + + it("08 a native take", async () => { + await post( + "08-native", + { takenAt: "2026-10-11T20:10:00.000Z" }, + { auth: ownerBearer }, + ); + }); + + it("09 a skip over a recorded dose, by the owner and by a delegate", async () => { + await post( + "09a-delegate-skip-refused", + { skipped: true, scheduledFor: "2026-10-14T20:00:00.000Z" }, + { ...writer, settle: () => refusalRows(2) }, + ); + await post("09b-owner-skip-flip", { + skipped: true, + scheduledFor: "2026-10-14T20:00:00.000Z", + }); + }); + + it("10 a delegate's take on an open slot, a reader, and nobody", async () => { + await post( + "10a-writer-take", + { takenAt: "2026-10-11T08:20:00.000Z" }, + writer, + ); + await post("10b-reader", { takenAt: "2026-10-10T08:20:00.000Z" }, reader); + await post( + "10c-anonymous", + { takenAt: "2026-10-10T08:20:00.000Z" }, + { auth: anonymous }, + ); + }); + + it("11 the injection-site refusal, and a site with a dose override", async () => { + await post( + "11a-site-not-allowed", + { takenAt: "2026-10-15T09:10:00.000Z", injectionSite: "ABDOMEN_RIGHT" }, + { medicationId: med.pen }, + ); + await post( + "11b-site-and-dose", + { + takenAt: "2026-10-15T09:10:00.000Z", + injectionSite: "ABDOMEN_LEFT", + doseTaken: "0.5 pen", + }, + { medicationId: med.pen }, + ); + }); + + it("12 a one-shot dose", async () => { + await post( + "12-one-shot", + { takenAt: "2026-10-15T10:00:00.000Z" }, + { medicationId: med.oneShot }, + ); + }); + + it("13 a body the schema refuses", async () => { + await post("13-invalid", { + takenAt: "yesterday", + idempotencyKey: "", + }); + }); + + it("14 another account's medication, and a record-only one", async () => { + await post( + "14a-not-found", + { takenAt: "2026-10-15T08:30:00.000Z" }, + { medicationId: med.foreign }, + ); + await post( + "14b-record-only", + { takenAt: "2026-10-15T07:10:00.000Z" }, + { medicationId: med.recordOnly }, + ); + }); +}); + +describe("GET /api/medications/{id}/intake parity", () => { + it("21 the default list", async () => { + await list("21-default"); + }); + + it("22 a completed page sorted by take time", async () => { + await list("22-completed-page", { + status: "completed", + sortBy: "takenAt", + sortDir: "asc", + limit: "3", + offset: "1", + }); + }); + + it("23 the skips, and the takes", async () => { + await list("23a-skipped", { status: "skipped" }); + await list("23b-taken", { status: "taken", sortDir: "asc" }); + }); + + it("24 a query the schema refuses", async () => { + await list("24-invalid", { limit: "lots", status: "maybe" }); + }); + + it("25 another account's medication, a reading delegate, and nobody", async () => { + await list("25a-not-found", {}, { medicationId: med.foreign }); + await list("25b-reader", {}, reader); + await list("25c-anonymous", {}, { auth: anonymous }); + }); +}); diff --git a/tests/integration/routes/parity/medication-state.ts b/tests/integration/routes/parity/medication-state.ts index b73829783..b96f6b348 100644 --- a/tests/integration/routes/parity/medication-state.ts +++ b/tests/integration/routes/parity/medication-state.ts @@ -210,6 +210,19 @@ export interface ParityCall { userIds: string[]; /** The clock the test pinned; server stamps at or after it read ``. */ now: Date; + /** + * Waits, after the response, for a write the route deliberately does not + * await (a refusal's audit row), so the rows read back do not depend on + * whether it landed first. + */ + settle?: () => Promise; + /** Further state the route writes outside the medication tables. */ + extra?: () => Promise; + /** + * Rewrites what the snapshot holds before the placeholders are applied, + * for a value that carries a generated id inside a longer string. + */ + rewrite?: (value: unknown) => unknown; } /** @@ -224,16 +237,18 @@ export async function parityStep(call: ParityCall) { const res = await callRoute(call.handler, call.options); const events = wideEventRecorder.take(); expect(events.length).toBeGreaterThan(0); + if (call.settle) await call.settle(); await expectParity( call.name, stable( - { + (call.rewrite ?? ((value: unknown) => value))({ status: res.status, body: res.body, events: events.map(eventFacts).map(withoutCacheKeyHashes), effects: effectRecorder.take(), stored: await medicationState(getPrismaClient(), call.userIds), - }, + ...(call.extra ? { extra: await call.extra() } : {}), + }), { since: call.now }, ), ); diff --git a/tests/integration/routes/parity/mood-entries-bulk.parity.test.ts b/tests/integration/routes/parity/mood-entries-bulk.parity.test.ts new file mode 100644 index 000000000..6c4a79519 --- /dev/null +++ b/tests/integration/routes/parity/mood-entries-bulk.parity.test.ts @@ -0,0 +1,249 @@ +/** + * Parity for `POST /api/mood-entries/bulk` while its ingest moves into + * `src/lib/mood/`. + * + * A first drain of mixed entries (with and without an external id, free + * tags, catalogue tags with an unknown key, a rated factor in and out of its + * scale, a day context, dimensions in and out of range, an unstable id), + * the same drain again, a re-post of an entry the person deleted, and the + * two whole-batch refusals. After each request the snapshot holds the + * response, the wide event and the stored entries with their links and + * context, a sealed note as its byte length. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { POST } from "@/app/api/mood-entries/bulk/route"; + +import { callRoute, type RouteAuth } from "../../../kit/call-route"; +import { makeSession, makeUser } from "../../../kit/factories"; +import { + eventFacts, + expectParity, + stable, + wideEventRecorder, +} from "../../../kit/parity"; +import { getPrismaClient, truncateAllTables } from "../../setup"; +import { makeCustomTag } from "../mood-tag-fixtures"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); + +const prisma = getPrismaClient(); +const NOW = new Date("2026-10-15T12:00:00.000Z"); +const SHORT_FACTOR_ID = "mt_parity_short_factor"; + +let userId: string; +let owner: RouteAuth; +let binaryKey: string; +let rated: { key: string; scaleMax: number }; +let customKey: string; + +async function stored() { + const [entries, user] = await Promise.all([ + prisma.moodEntry.findMany({ + where: { userId }, + orderBy: [{ moodLoggedAt: "asc" }, { source: "asc" }], + select: { + id: true, + date: true, + tz: true, + mood: true, + score: true, + moodA1: true, + stressA2: true, + energyA3: true, + connectionA4: true, + stabilityA5: true, + tags: true, + note: true, + noteEncrypted: true, + source: true, + externalId: true, + moodLoggedAt: true, + deletedAt: true, + tagLinks: { + orderBy: { moodTag: { key: "asc" } }, + select: { moodTag: { select: { key: true } }, rating: true }, + }, + context: true, + }, + }), + prisma.user.findUniqueOrThrow({ + where: { id: userId }, + select: { lastSyncedAt: true }, + }), + ]); + return { entries, user }; +} + +async function step(name: string, body: unknown): Promise { + wideEventRecorder.take(); + const res = await callRoute(POST, { + method: "POST", + route: "/api/mood-entries/bulk", + body, + auth: owner, + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + await expectParity( + `mood-entries-bulk.${name}`, + withoutMintedKey( + stable( + { + status: res.status, + body: res.body, + events: events.map(eventFacts), + stored: await stored(), + }, + { since: NOW }, + ), + ), + ); +} + +/** + * The account's own tag carries a key minted at random (`custom:`); + * every occurrence is written as one placeholder so the snapshot is the same + * on each run. + */ +function withoutMintedKey(value: unknown): unknown { + return JSON.parse( + JSON.stringify(value).split(customKey).join(""), + ); +} + +function entry( + at: string, + mood: string, + extra: Record = {}, +): Record { + return { mood, moodLoggedAt: at, ...extra }; +} + +let firstDrain: Record[]; + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const user = await makeUser({ timezone: "Europe/Berlin" }); + userId = user.id; + owner = await makeSession(user); + const binary = await prisma.moodTag.findFirstOrThrow({ + where: { userId: null, kind: "BINARY", isActive: true }, + orderBy: { key: "asc" }, + select: { key: true }, + }); + binaryKey = binary.key; + // A catalogue factor on a scale shorter than the envelope's 1..5, so a + // rating the envelope admits can still fall outside the factor's scale. + // Every live catalogue factor runs 1..5, so this file adds one with a + // fixed key and removes it afterwards. + const ratedTag = await prisma.moodTag.create({ + data: { + id: SHORT_FACTOR_ID, + key: "factor_parity_short", + labelKey: "mood.tag.factorParityShort", + categoryId: "mtc_factors", + kind: "RATED", + scaleMin: 1, + scaleMax: 2, + }, + select: { key: true, scaleMax: true }, + }); + rated = ratedTag; + // Rated factors resolve against the catalogue only, so one the account + // made itself is reported as dropped. + const custom = await makeCustomTag(user, "sleep debt", { + kind: "RATED", + scaleMin: 1, + scaleMax: 2, + }); + customKey = custom.key; + firstDrain = [ + entry("2026-10-14T07:00:00.000Z", "GUT", { + externalId: "sd-1", + source: "WEB", + tags: ["slept well"], + tagKeys: [binaryKey, "no_such_tag"], + ratedFactors: [ + { key: rated.key, rating: 1 }, + { key: customKey, rating: 1 }, + ], + note: "a calm morning", + a2: 7, + a3: 4, + }), + entry("2026-10-14T19:30:00.000Z", "OKAY"), + entry("2026-10-13T21:00:00.000Z", "SCHLECHT", { + externalId: "sd-2", + context: { workStatus: "regular", workMinutes: 480 }, + }), + entry("2026-10-12T09:00:00.000Z", "SUPER_GUT", { + externalId: "sd-3", + ratedFactors: [{ key: rated.key, rating: rated.scaleMax + 1 }], + }), + entry("2026-10-12T10:00:00.000Z", "GUT", { + externalId: "sd-4", + context: { notAKey: true }, + }), + entry("2026-10-12T11:00:00.000Z", "GUT", { + externalId: "sd-5", + a4: 11, + }), + entry("2026-10-12T12:00:00.000Z", "GUT", { + externalId: "", + }), + ]; +}); + +afterAll(async () => { + vi.useRealTimers(); + await prisma.moodEntryTagLink.deleteMany({ + where: { moodTagId: SHORT_FACTOR_ID }, + }); + await prisma.moodTag.delete({ where: { id: SHORT_FACTOR_ID } }); +}); + +describe("POST /api/mood-entries/bulk parity", () => { + it("01 a first drain", async () => { + await step("01-first-drain", { entries: firstDrain }); + }); + + it("02 the same drain again", async () => { + await step("02-same-drain", { entries: firstDrain }); + }); + + it("03 a re-post of an entry the person deleted", async () => { + await prisma.moodEntry.updateMany({ + where: { userId, externalId: "sd-2" }, + data: { deletedAt: new Date("2026-10-14T20:00:00.000Z") }, + }); + await step("03-tombstone", { + entries: [ + entry("2026-10-13T21:00:00.000Z", "OKAY", { externalId: "sd-2" }), + ], + }); + }); + + it("04 the whole-batch refusals", async () => { + await step("04-too-large", { + entries: Array.from({ length: 501 }, (_, i) => + entry( + new Date(NOW.getTime() - (i + 1) * 3_600_000).toISOString(), + "OKAY", + ), + ), + }); + await step("05-invalid", { entries: [{ mood: "ECSTATIC" }] }); + }); +}); diff --git a/tests/integration/routes/parity/oidc-callback.parity.test.ts b/tests/integration/routes/parity/oidc-callback.parity.test.ts new file mode 100644 index 000000000..3f4145bbf --- /dev/null +++ b/tests/integration/routes/parity/oidc-callback.parity.test.ts @@ -0,0 +1,395 @@ +/** + * Parity for `GET /api/auth/oidc/callback` while its sign-in resolution moves + * into `src/lib/auth/`. + * + * The provider is replaced at the four calls that would leave the process + * (discovery, code exchange, ID-token verification, userinfo); everything + * else is real: the sealed state cookie, sessions, MFA challenges, native + * handoffs, the pending-link cookie, the audit ledger. The walk covers each + * outcome the route tells apart: SSO switched off, a provider refusal, a + * broken round trip, a missing state blob with and without a live session, + * a forged state, a failed exchange, an exchange with no ID token, + * first-login provisioning (the first account becomes admin), a returning + * identity whose verified email changed, the userinfo fallback, the email + * refusals, a link conflict, a pending link on the web and on the phone, + * closed registration, the second factor on the web and on the phone, the + * native handoff, and the two claim refusals. After each request the + * snapshot holds the redirect, the cookies it set, the wide event, the + * recorded sign-in device tail and the accounts, sessions and ledger rows. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { GET } from "@/app/api/auth/oidc/callback/route"; +import { encrypt } from "@/lib/crypto"; +import { OIDC_STATE_COOKIE } from "@/lib/auth/oidc-cookie"; +import { SESSION_COOKIE_NAME } from "@/lib/auth/session"; + +import { anonymous, callRoute } from "../../../kit/call-route"; +import { makeSession, makeUser } from "../../../kit/factories"; +import { + effectRecorder, + eventFacts, + expectParity, + stable, + wideEventRecorder, +} from "../../../kit/parity"; +import { getPrismaClient, truncateAllTables } from "../../setup"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); +vi.mock("@/lib/auth/login-alert", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + recordSignInDevice: async (...args: unknown[]) => { + effectRecorder.record("recordSignInDevice", args); + }, + }; +}); + +/** What the fake provider answers for the next callback. */ +const idp = vi.hoisted(() => ({ + exchange: "ok" as "ok" | "no_id_token" | "throws", + accessToken: false, + identity: { + sub: "sub-1", + email: null as string | null, + emailVerified: undefined as boolean | undefined, + }, + userinfo: { + email: null as string | null, + emailVerified: undefined as boolean | undefined, + }, +})); +const ISSUER = "https://id.example.test"; +vi.mock("@/lib/auth/oidc", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + discoverOidcMetadata: async () => ({ + issuer: "https://id.example.test", + authorization_endpoint: "https://id.example.test/authorize", + token_endpoint: "https://id.example.test/token", + jwks_uri: "https://id.example.test/jwks", + }), + exchangeCodeForTokens: async () => { + if (idp.exchange === "throws") throw new Error("invalid_grant"); + return { + ...(idp.exchange === "ok" ? { id_token: "id-token" } : {}), + ...(idp.accessToken ? { access_token: "access-token" } : {}), + token_type: "Bearer", + }; + }, + verifyIdToken: async () => ({ ...idp.identity, name: null }), + fetchUserinfoEmail: async () => ({ ...idp.userinfo }), + }; +}); + +const prisma = getPrismaClient(); +const NOW = new Date("2026-10-15T12:00:00.000Z"); +const APP = "http://localhost:3000"; +const SAVED_ENV = { ...process.env }; + +interface State { + native?: true; + appCodeChallenge?: string; + intent?: "claim"; + claimBinding?: string; + next?: string; + /** Send a different `state` in the query than the blob carries. */ + forged?: true; +} + +function stateCookie(state: State): string { + return encrypt( + JSON.stringify({ + state: "state-1", + nonce: "nonce-1", + codeVerifier: "verifier-1", + next: state.next ?? "/", + ...(state.native + ? { native: true, appCodeChallenge: state.appCodeChallenge } + : {}), + ...(state.intent + ? { intent: state.intent, claimBinding: state.claimBinding } + : {}), + }), + ); +} + +/** The redirect target with its one-time values replaced. */ +function location(headers: Headers): string | null { + const raw = headers.get("location"); + if (!raw) return null; + return raw.replace( + /([?&](?:code|mfa_ticket)=)[^&]+/g, + (_, key: string) => `${key}`, + ); +} + +/** The cookies the response set or cleared, by name and attributes. */ +function setCookies(headers: Headers): string[] { + return headers + .getSetCookie() + .map((c) => + c.replace( + /^([^=]+)=([^;]*)/, + (_, n: string, v: string) => `${n}=${v ? "" : ""}`, + ), + ) + .map((c) => c.replace(/Expires=[^;]+/i, "Expires=")) + .sort(); +} + +async function stored() { + const [users, sessions, audit, challenges, handoffs] = await Promise.all([ + prisma.user.findMany({ + orderBy: { username: "asc" }, + select: { + username: true, + email: true, + role: true, + timezone: true, + oidcIssuer: true, + oidcSub: true, + }, + }), + prisma.session.findMany({ + orderBy: [{ createdAt: "asc" }, { userId: "asc" }], + select: { + user: { select: { username: true } }, + mfaVerifiedAt: true, + }, + }), + prisma.auditLog.findMany({ + orderBy: [{ createdAt: "asc" }, { action: "asc" }], + select: { action: true, user: { select: { username: true } } }, + }), + prisma.mfaChallenge.count(), + prisma.oidcNativeHandoff.count(), + ]); + return { users, sessions, audit, challenges, handoffs }; +} + +async function step( + name: string, + options: { + query?: Record; + state?: State | null; + cookies?: Record; + }, +): Promise { + await prisma.rateLimit.deleteMany({}); + wideEventRecorder.take(); + effectRecorder.take(); + const query = + options.query ?? + ({ + code: "code-1", + state: options.state?.forged ? "state-x" : "state-1", + } as Record); + const cookies: Record = { + ...(options.state + ? { [OIDC_STATE_COOKIE]: stateCookie(options.state) } + : {}), + ...options.cookies, + }; + const res = await callRoute(GET, { + method: "GET", + route: "/api/auth/oidc/callback", + query, + headers: { + cookie: Object.entries(cookies) + .map(([k, v]) => `${k}=${v}`) + .join("; "), + }, + auth: anonymous, + contract: { + skip: "a browser redirect target, not part of the published API", + }, + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + await expectParity( + `oidc-callback.${name}`, + stable( + { + status: res.status, + location: location(res.headers), + setCookies: setCookies(res.headers), + events: events.map(eventFacts), + effects: effectRecorder.take(), + stored: await stored(), + }, + { since: NOW }, + ), + ); +} + +function identity( + sub: string, + email: string | null, + emailVerified: boolean | undefined, +): void { + idp.exchange = "ok"; + idp.accessToken = false; + idp.identity = { sub, email, emailVerified }; +} + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + process.env.APP_URL = APP; + process.env.NEXT_PUBLIC_APP_URL = APP; + delete process.env.OIDC_ISSUER_URL; + delete process.env.OIDC_ONLY; +}); + +afterAll(() => { + vi.useRealTimers(); + for (const key of [ + "APP_URL", + "NEXT_PUBLIC_APP_URL", + "OIDC_ISSUER_URL", + "OIDC_CLIENT_ID", + "OIDC_CLIENT_SECRET", + ]) { + if (SAVED_ENV[key] === undefined) delete process.env[key]; + else process.env[key] = SAVED_ENV[key]; + } +}); + +describe("GET /api/auth/oidc/callback parity", () => { + it("01 SSO switched off", async () => { + await step("01-disabled", { state: {} }); + process.env.OIDC_ISSUER_URL = ISSUER; + process.env.OIDC_CLIENT_ID = "healthlog-test"; + process.env.OIDC_CLIENT_SECRET = "client-secret"; + }); + + it("02 the round-trip refusals", async () => { + await step("02-idp-denied", { + query: { error: "access_denied", state: "state-1" }, + state: {}, + }); + await step("03-idp-denied-unknown-code", { + query: { error: "something_else" }, + state: { native: true, appCodeChallenge: "challenge-1" }, + }); + await step("04-no-code", { query: { state: "state-1" }, state: {} }); + await step("05-no-state-blob", { state: null }); + await step("06-forged-state", { state: { forged: true } }); + idp.exchange = "throws"; + await step("07-exchange-failed", { state: {} }); + idp.exchange = "no_id_token"; + await step("08-no-id-token", { state: {} }); + }); + + it("09 the first login provisions an admin", async () => { + identity("sub-first", "First@Example.test", true); + await step("09-provision-first", { state: { next: "/insights" } }); + identity("sub-second", "second@example.test", true); + await step("10-provision-second", { state: {} }); + }); + + it("11 a returning identity, and a duplicate callback", async () => { + identity("sub-first", "first.new@example.test", true); + await step("11-returning-email-refresh", { state: {} }); + const first = await prisma.user.findFirstOrThrow({ + where: { oidcSub: "sub-first" }, + }); + const session = await makeSession(first); + await step("12-duplicate-with-session", { + state: null, + cookies: { [SESSION_COOKIE_NAME]: session.cookie }, + }); + }); + + it("13 the email refusals and the userinfo fallback", async () => { + identity("sub-noemail", null, undefined); + await step("13-no-email", { state: {} }); + identity("sub-unverified", "unverified@example.test", false); + await step("14-email-unverified", { state: {} }); + identity("sub-userinfo", null, undefined); + idp.accessToken = true; + idp.userinfo = { email: "userinfo@example.test", emailVerified: true }; + await step("15-userinfo-provision", { state: {} }); + }); + + it("16 an account already holding the email", async () => { + await makeUser({ + username: "linked-elsewhere", + email: "conflict@example.test", + oidcIssuer: "https://other.example.test", + oidcSub: "other-sub", + }); + identity("sub-conflict", "conflict@example.test", true); + await step("16-link-conflict", { state: {} }); + await makeUser({ username: "local-only", email: "local@example.test" }); + identity("sub-local", "local@example.test", true); + await step("17-link-pending-web", { state: {} }); + await step("18-link-pending-native", { + state: { native: true, appCodeChallenge: "challenge-1" }, + }); + }); + + it("19 closed registration", async () => { + await prisma.appSettings.upsert({ + where: { id: "singleton" }, + create: { id: "singleton", registrationEnabled: false }, + update: { registrationEnabled: false }, + }); + identity("sub-late", "late@example.test", true); + await step("19-registration-closed", { state: {} }); + await prisma.appSettings.update({ + where: { id: "singleton" }, + data: { registrationEnabled: true }, + }); + }); + + it("20 the second factor and the native handoff", async () => { + await prisma.user.updateMany({ + where: { oidcSub: "sub-second" }, + data: { totpConfirmedAt: new Date("2026-09-01T00:00:00.000Z") }, + }); + identity("sub-second", "second@example.test", true); + await step("20-mfa-web", { state: { next: "/medications" } }); + await step("21-mfa-native", { + state: { native: true, appCodeChallenge: "challenge-1" }, + }); + identity("sub-userinfo", "userinfo@example.test", true); + await step("22-native-handoff", { + state: { native: true, appCodeChallenge: "challenge-2" }, + }); + }); + + it("23 the claim refusals", async () => { + const someone = await prisma.user.findFirstOrThrow({ + where: { oidcSub: "sub-first" }, + }); + const session = await makeSession(someone); + identity("sub-claimer", "claimer@example.test", true); + await step("23-claim-signed-in", { + state: { intent: "claim", claimBinding: "binding-1" }, + cookies: { [SESSION_COOKIE_NAME]: session.cookie }, + }); + await step("24-claim-no-cookie", { + state: { intent: "claim", claimBinding: "binding-1" }, + }); + await step("25-claim-no-code", { + query: { state: "state-1" }, + state: { intent: "claim", claimBinding: "binding-1" }, + }); + }); +}); diff --git a/tests/integration/routes/parity/record-fixture.ts b/tests/integration/routes/parity/record-fixture.ts new file mode 100644 index 000000000..40ad3cfe4 --- /dev/null +++ b/tests/integration/routes/parity/record-fixture.ts @@ -0,0 +1,294 @@ +/** + * One representative health record for the read-route parity walks. + * + * The aggregate routes (`/api/analytics`, `/api/dashboard/summary`, + * `/api/insights/comprehensive`, `/api/insights/generate`) shape their body + * from many domains at once, so a parity walk over them needs a record in + * which every block has something to say: daily weight and blood pressure + * long enough for the 90-day windows, a resting-pulse series, glucose in + * three meal contexts (one untagged), staged sleep nights with a nap, steps, + * oxygen saturation, HRV, a mood diary, a blood-pressure medication with an + * intake history that has skips and gaps, a second medication, and an open + * illness episode. + * + * Every value is a function of the day offset from `now`, so the record is + * the same on every run. Medications carry fixed ids, because some payloads + * hash or echo them; measurement ids are generated and blanked by `stable`. + * + * Not a test file. Only for tests under `tests/integration/`. + */ +import type { Prisma } from "@/generated/prisma/client"; + +import { getPrismaClient } from "../../setup"; + +const DAY_MS = 24 * 60 * 60 * 1000; + +/** A cuid-shaped fixed id: `c` and 24 lower-case alphanumerics. */ +export function parityId(tag: string, n: number): string { + return `c${`${tag}${n.toString(36)}`.padStart(24, "0")}`; +} + +/** A local `YYYY-MM-DD` key of `at` in `tz`. */ +function dayKey(at: Date, tz: string): string { + return new Intl.DateTimeFormat("en-CA", { + timeZone: tz, + year: "numeric", + month: "2-digit", + day: "2-digit", + }).format(at); +} + +/** The UTC instant `hh:mm` on the day `d` days before `now`. */ +function at(now: Date, d: number, hh: number, mm = 0): Date { + const base = new Date(now.getTime() - d * DAY_MS); + base.setUTCHours(hh, mm, 0, 0); + return base; +} + +export interface RecordFixture { + bpMedicationId: string; + otherMedicationId: string; +} + +/** + * Write the record onto an existing account. `tag` keeps the fixed ids of + * two records in one database apart. + */ +export async function seedParityRecord(options: { + userId: string; + now: Date; + tz: string; + tag: string; +}): Promise { + const { userId, now, tz, tag } = options; + const prisma = getPrismaClient(); + const rows: Prisma.MeasurementCreateManyInput[] = []; + const push = (row: Omit) => + rows.push({ userId, createdAt: row.measuredAt, ...row }); + + for (let d = 0; d < 60; d++) { + push({ + type: "WEIGHT", + value: Math.round((82 + d * 0.04) * 10) / 10, + unit: "kg", + source: "WITHINGS", + deviceType: "scale", + measuredAt: at(now, d, 6, 30), + }); + push({ + type: "PULSE", + value: 64 + (d % 6), + unit: "bpm", + measuredAt: at(now, d, 7, 1), + }); + if (d % 7 === 0) { + push({ + type: "BODY_FAT", + value: Math.round((24 + d * 0.02) * 10) / 10, + unit: "%", + source: "WITHINGS", + measuredAt: at(now, d, 6, 31), + }); + } + } + for (let d = 0; d < 120; d++) { + push({ + type: "BLOOD_PRESSURE_SYS", + value: 123 + ((d * 7) % 15), + unit: "mmHg", + measuredAt: at(now, d, 7, 0), + }); + push({ + type: "BLOOD_PRESSURE_DIA", + value: 78 + ((d * 5) % 9), + unit: "mmHg", + measuredAt: at(now, d, 7, 0), + }); + } + for (let d = 0; d < 40; d++) { + push({ + type: "RESTING_HEART_RATE", + value: 58 + (d % 4), + unit: "bpm", + source: "APPLE_HEALTH", + measuredAt: at(now, d, 5, 0), + }); + } + for (let d = 0; d < 20; d++) { + push({ + type: "BLOOD_GLUCOSE", + value: 92 + (d % 7), + unit: "mg/dL", + glucoseContext: "FASTING", + measuredAt: at(now, d, 6, 15), + }); + push({ + type: "BLOOD_GLUCOSE", + value: 128 + ((d * 3) % 20), + unit: "mg/dL", + glucoseContext: "POSTPRANDIAL", + measuredAt: at(now, d, 11, 0), + }); + push({ + type: "BLOOD_GLUCOSE", + value: 105 + (d % 4), + unit: "mg/dL", + measuredAt: at(now, d, 10, 0), + }); + } + // Staged nights ending on the morning of each wake day, and one nap. + for (let d = 0; d < 21; d++) { + const stage = ( + sleepStage: "CORE" | "DEEP" | "REM" | "AWAKE", + hh: number, + mm: number, + minutes: number, + ) => + push({ + type: "SLEEP_DURATION", + value: minutes, + unit: "min", + source: "APPLE_HEALTH", + deviceType: "watch", + sleepStage, + measuredAt: at(now, d, hh, mm), + }); + stage("CORE", 1, 0, 200 + (d % 5) * 10); + stage("DEEP", 2, 30, 70 + (d % 3) * 5); + stage("REM", 4, 0, 85 + (d % 4) * 5); + stage("AWAKE", 4, 15, 12); + } + push({ + type: "SLEEP_DURATION", + value: 35, + unit: "min", + source: "APPLE_HEALTH", + deviceType: "watch", + sleepStage: "CORE", + measuredAt: at(now, 2, 13, 40), + }); + for (let d = 0; d < 30; d++) { + push({ + type: "ACTIVITY_STEPS", + value: 6000 + ((d * 373) % 4000), + unit: "count", + source: "APPLE_HEALTH", + measuredAt: at(now, d, 9, 0), + }); + push({ + type: "HEART_RATE_VARIABILITY", + value: 45 + (d % 8), + unit: "ms", + source: "APPLE_HEALTH", + measuredAt: at(now, d, 4, 30), + }); + } + for (let d = 0; d < 10; d++) { + push({ + type: "OXYGEN_SATURATION", + value: 96 + (d % 3), + unit: "%", + source: "APPLE_HEALTH", + measuredAt: at(now, d, 4, 40), + }); + } + await prisma.measurement.createMany({ data: rows }); + + const moods = ["LAUSIG", "SCHLECHT", "OKAY", "GUT", "SUPER_GUT"]; + const moodRows: Prisma.MoodEntryCreateManyInput[] = []; + for (let d = 0; d < 45; d++) { + const score = 1 + ((d * 3) % 5); + const loggedAt = at(now, d, 9, 30); + moodRows.push({ + userId, + date: dayKey(loggedAt, tz), + mood: moods[score - 1], + score, + moodLoggedAt: loggedAt, + tz, + createdAt: loggedAt, + }); + } + await prisma.moodEntry.createMany({ data: moodRows }); + + const bpMedicationId = parityId(`${tag}med`, 1); + const otherMedicationId = parityId(`${tag}med`, 2); + const created = new Date(now.getTime() - 90 * DAY_MS); + await prisma.medication.create({ + data: { + id: bpMedicationId, + userId, + name: "Ramipril", + dose: "5mg", + createdAt: created, + schedules: { + create: [ + { + id: parityId(`${tag}sch`, 1), + windowStart: "08:00", + windowEnd: "10:00", + }, + ], + }, + categoryAssignment: { create: { category: "BLOOD_PRESSURE" } }, + }, + }); + await prisma.medication.create({ + data: { + id: otherMedicationId, + userId, + name: "Vitamin D", + dose: "1000 IU", + createdAt: created, + schedules: { + create: [ + { + id: parityId(`${tag}sch`, 2), + windowStart: "20:00", + windowEnd: "21:00", + }, + ], + }, + }, + }); + const intakes: Prisma.MedicationIntakeEventCreateManyInput[] = []; + for (let d = 1; d <= 45; d++) { + if (d % 9 === 0) continue; + const scheduledFor = at(now, d, 6, 0); + const skipped = d % 7 === 0; + intakes.push({ + id: parityId(`${tag}ie`, d), + userId, + medicationId: bpMedicationId, + scheduledFor, + takenAt: skipped ? null : at(now, d, 6, 20), + skipped, + createdAt: scheduledFor, + }); + if (d % 2 === 0) { + const evening = at(now, d, 18, 0); + intakes.push({ + id: parityId(`${tag}ie`, 100 + d), + userId, + medicationId: otherMedicationId, + scheduledFor: evening, + takenAt: at(now, d, 18, 30), + createdAt: evening, + }); + } + } + await prisma.medicationIntakeEvent.createMany({ data: intakes }); + + await prisma.illnessEpisode.create({ + data: { + id: parityId(`${tag}ill`, 1), + userId, + label: "Common cold", + type: "INFECTION", + onsetAt: new Date(now.getTime() - 3 * DAY_MS), + createdAt: new Date(now.getTime() - 3 * DAY_MS), + }, + }); + + return { bpMedicationId, otherMedicationId }; +} diff --git a/tests/integration/routes/parity/sync-changes.parity.test.ts b/tests/integration/routes/parity/sync-changes.parity.test.ts new file mode 100644 index 000000000..d4d092ff9 --- /dev/null +++ b/tests/integration/routes/parity/sync-changes.parity.test.ts @@ -0,0 +1,355 @@ +/** + * Parity for `GET /api/sync/changes` while its domain pages move into + * `src/lib/sync/`. + * + * One account holds live and deleted rows in all five domains (a sealed note + * on a measurement and a mood entry, a mood context, a recorded and an + * auto-recorded intake, cycle days and cycles), plus a raw sample that a live + * hourly row folded away. The walk drains the feed in small pages to the + * end, pulls once caught up, then asks with a cursor past retention, a + * cursor from before a reset, the cycle module switched off, and a refused + * query. Every row carries a fixed id and `updatedAt`, so the keyset order + * and the opaque cursor are the same on every run. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { GET } from "@/app/api/sync/changes/route"; +import { encryptNote } from "@/lib/crypto/note-cipher"; +import { encodeCursor } from "@/lib/sync/cursor"; + +import { callRoute, type RouteAuth } from "../../../kit/call-route"; +import { makeMedication, makeSession, makeUser } from "../../../kit/factories"; +import { + eventFacts, + expectParity, + stable, + wideEventRecorder, +} from "../../../kit/parity"; +import { getPrismaClient, truncateAllTables } from "../../setup"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); + +const prisma = getPrismaClient(); +const NOW = new Date("2026-10-15T12:00:00.000Z"); +const MIN = 60_000; +const DAY = 86_400_000; + +let userId: string; +let owner: RouteAuth; + +/** An `updatedAt` some minutes before the clock, one per row. */ +function touched(minutesAgo: number): Date { + return new Date(NOW.getTime() - minutesAgo * MIN); +} + +async function seed(): Promise { + const deletedAt = touched(30); + await prisma.measurement.createMany({ + data: [ + { + id: "m-01", + userId, + type: "WEIGHT", + value: 81.2, + unit: "kg", + measuredAt: new Date("2026-10-14T06:30:00.000Z"), + source: "MANUAL", + notesEncrypted: encryptNote("after the run"), + updatedAt: touched(300), + }, + { + id: "m-02", + userId, + type: "PULSE", + value: 61, + unit: "bpm", + measuredAt: new Date("2026-10-14T07:00:00.000Z"), + source: "WITHINGS", + externalId: "w-pulse-1", + updatedAt: touched(290), + }, + { + id: "m-03", + userId, + type: "WEIGHT", + value: 81.6, + unit: "kg", + measuredAt: new Date("2026-10-13T06:30:00.000Z"), + source: "WITHINGS", + externalId: "w-weight-2", + deletedAt, + updatedAt: touched(280), + }, + // The hourly row and a raw sample it folded away, four months back: + // the sample's tombstone is the fold's, not a deletion anyone made. + { + id: "m-04", + userId, + type: "PULSE", + value: 64, + unit: "bpm", + measuredAt: new Date("2026-06-17T09:30:00.000Z"), + source: "APPLE_HEALTH", + externalId: "stats:HKQuantityTypeIdentifierHeartRate:2026-06-17T09", + updatedAt: touched(270), + }, + { + id: "m-05", + userId, + type: "PULSE", + value: 66, + unit: "bpm", + measuredAt: new Date("2026-06-17T09:12:00.000Z"), + source: "APPLE_HEALTH", + externalId: "uuid-raw-pulse-1", + deletedAt: touched(260), + updatedAt: touched(260), + }, + { + id: "m-06", + userId, + type: "BLOOD_PRESSURE_SYS", + value: 128, + unit: "mmHg", + measuredAt: new Date("2026-10-14T08:00:00.000Z"), + source: "MANUAL", + updatedAt: touched(250), + }, + ], + }); + await prisma.moodEntry.createMany({ + data: [ + { + id: "mood-01", + userId, + date: "2026-10-14", + tz: "Europe/Berlin", + mood: "GUT", + score: 4, + moodA1: 7, + stressA2: 3, + tags: JSON.stringify(["slept well"]), + noteEncrypted: encryptNote("a calm morning"), + moodLoggedAt: new Date("2026-10-14T07:00:00.000Z"), + source: "MANUAL", + updatedAt: touched(240), + }, + { + id: "mood-02", + userId, + date: "2026-10-13", + tz: "Europe/Berlin", + mood: "SCHLECHT", + score: 2, + moodA1: 3, + moodLoggedAt: new Date("2026-10-13T20:00:00.000Z"), + source: "MANUAL", + deletedAt, + updatedAt: touched(230), + }, + ], + }); + await prisma.moodContext.create({ + data: { + moodEntryId: "mood-01", + userId, + workStatus: "regular", + workMinutes: 480, + }, + }); + const medication = await makeMedication({ id: userId }); + await prisma.medicationIntakeEvent.createMany({ + data: [ + { + id: "in-01", + userId, + medicationId: medication.id, + scheduledFor: new Date("2026-10-14T06:00:00.000Z"), + takenAt: new Date("2026-10-14T06:10:00.000Z"), + source: "WEB", + updatedAt: touched(220), + }, + { + id: "in-02", + userId, + medicationId: medication.id, + scheduledFor: new Date("2026-10-13T06:00:00.000Z"), + takenAt: new Date("2026-10-13T06:00:00.000Z"), + autoRecorded: true, + source: "WEB", + updatedAt: touched(210), + }, + { + id: "in-03", + userId, + medicationId: medication.id, + scheduledFor: new Date("2026-10-12T06:00:00.000Z"), + skipped: true, + source: "WEB", + deletedAt, + updatedAt: touched(200), + }, + ], + }); + await prisma.menstrualCycle.createMany({ + data: [ + { + id: "cy-01", + userId, + startDate: "2026-09-12", + endDate: "2026-10-09", + lengthDays: 28, + periodEndDate: "2026-09-16", + updatedAt: touched(190), + }, + { + id: "cy-02", + userId, + startDate: "2026-10-10", + updatedAt: touched(180), + }, + { + id: "cy-03", + userId, + startDate: "2026-08-01", + deletedAt, + updatedAt: touched(170), + }, + ], + }); + await prisma.cycleDayLog.createMany({ + data: [ + { + id: "cd-01", + userId, + date: "2026-10-10", + cycleId: "cy-02", + flow: "MEDIUM", + updatedAt: touched(160), + }, + { + id: "cd-02", + userId, + date: "2026-10-11", + cycleId: "cy-02", + flow: "LIGHT", + basalBodyTempC: 36.4, + externalId: "hk-cycle-1", + updatedAt: touched(150), + }, + { + id: "cd-03", + userId, + date: "2026-10-09", + externalId: "hk-cycle-0", + deletedAt, + updatedAt: touched(140), + }, + ], + }); +} + +type Feed = { cursor: string | null; hasMore: boolean }; + +async function pull( + name: string, + query: Record, +): Promise { + wideEventRecorder.take(); + const res = await callRoute(GET, { + method: "GET", + route: "/api/sync/changes", + query, + auth: owner, + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + await expectParity( + `sync-changes.${name}`, + stable( + { status: res.status, body: res.body, events: events.map(eventFacts) }, + { since: NOW }, + ), + ); + return res.data; +} + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const user = await makeUser({ gender: "FEMALE", timezone: "UTC" }); + userId = user.id; + owner = await makeSession(user); + await prisma.cycleProfile.create({ data: { userId } }); + await seed(); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("GET /api/sync/changes parity", () => { + let firstCursor: string; + + it("drains the feed in small pages to the end", async () => { + const first = await pull("01-first-page", { limit: 2 }); + expect(first.hasMore).toBe(true); + firstCursor = first.cursor!; + const second = await pull("02-second-page", { + limit: 2, + cursor: first.cursor!, + }); + const rest = await pull("03-the-rest", { + limit: 500, + cursor: second.cursor!, + }); + expect(rest.hasMore).toBe(false); + await pull("04-caught-up", { cursor: rest.cursor! }); + }); + + it("a cursor past retention, and one from before a reset", async () => { + await pull("05-past-retention", { + cursor: encodeCursor( + { + measurements: { updatedAtMs: NOW.getTime() - 200 * DAY, id: "m-01" }, + }, + NOW.getTime(), + ), + }); + await prisma.user.update({ + where: { id: userId }, + data: { syncResetAt: new Date(NOW.getTime() + MIN) }, + }); + await pull("06-before-reset", { cursor: firstCursor }); + await prisma.user.update({ + where: { id: userId }, + data: { syncResetAt: null }, + }); + }); + + it("the cycle module switched off", async () => { + await prisma.cycleProfile.update({ + where: { userId }, + data: { cycleTrackingEnabled: false }, + }); + await pull("07-cycle-off", { limit: 500 }); + await prisma.cycleProfile.update({ + where: { userId }, + data: { cycleTrackingEnabled: null }, + }); + }); + + it("a refused query", async () => { + await pull("08-invalid", { limit: 0 }); + }); +}); diff --git a/tests/integration/routes/parity/workout-detail.parity.test.ts b/tests/integration/routes/parity/workout-detail.parity.test.ts new file mode 100644 index 000000000..3b5e8debf --- /dev/null +++ b/tests/integration/routes/parity/workout-detail.parity.test.ts @@ -0,0 +1,311 @@ +/** + * Parity for `GET` and `DELETE /api/workouts/{id}` while their logic moves + * into `src/lib/workouts/`. + * + * Workouts land through the real batch ingest (a HealthKit run with its + * heart-rate series and a kept track, the same run entered by hand, an + * earlier run, a ride), then the detail read walks the cases it tells apart: + * the canonical row, its hidden twin, the compact body, another account's + * row, and the module switched off. The delete walk covers a hand-entered + * workout with the record it set, a synced workout and a missing one. Each + * snapshot holds the response, the wide event, the recorded queue tail and, + * for the deletes, the workouts and records read back. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { DELETE, GET } from "@/app/api/workouts/[id]/route"; +import { POST as postBatch } from "@/app/api/workouts/batch/route"; + +import { callRoute, type RouteAuth } from "../../../kit/call-route"; +import { makeSession, makeUser } from "../../../kit/factories"; +import { + effectRecorder, + eventFacts, + expectParity, + stable, + wideEventRecorder, +} from "../../../kit/parity"; +import { getPrismaClient, truncateAllTables } from "../../setup"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); +vi.mock("@/lib/arrivals/emit-shared", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + emitDataArrival: async (...args: unknown[]) => { + effectRecorder.record("emitDataArrival", args); + }, + }; +}); +vi.mock("@/lib/jobs/pr-detection", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + enqueuePrDetection: async (...args: unknown[]) => { + effectRecorder.record("enqueuePrDetection", args); + }, + }; +}); + +const prisma = getPrismaClient(); +const NOW = new Date("2026-10-15T12:00:00.000Z"); + +let userId: string; +let owner: RouteAuth; +let stranger: RouteAuth; +const ids: Record = {}; + +const SERIES = [ + { t: "2026-10-14T06:30:00.000Z", hr: 121 }, + { t: "2026-10-14T06:35:00.000Z", hr: 142 }, + { t: "2026-10-14T06:40:00.000Z", hr: 151 }, + { t: "2026-10-14T06:45:00.000Z", hr: 147 }, +]; + +const TRACK = { + geometry: { + type: "LineString", + coordinates: [ + [7.2162, 51.4818, 98], + [7.2262, 51.4868, 104], + [7.2383, 51.4929, 101], + ], + }, + sampleTimestamps: [ + { t: "2026-10-14T06:30:00.000Z", hr: 120 }, + { t: "2026-10-14T06:38:00.000Z", hr: 141 }, + { t: "2026-10-14T06:46:00.000Z", hr: 150 }, + ], +}; + +function workout( + externalId: string, + start: string, + minutes: number, + extra: Record = {}, +) { + const startedAt = new Date(start); + return { + sportType: "running", + startedAt: startedAt.toISOString(), + endedAt: new Date(startedAt.getTime() + minutes * 60_000).toISOString(), + source: "APPLE_HEALTH", + externalId, + ...extra, + }; +} + +async function idOf(externalId: string): Promise { + const row = await prisma.workout.findFirstOrThrow({ + where: { userId, externalId }, + select: { id: true }, + }); + return row.id; +} + +async function stored() { + const [workouts, records] = await Promise.all([ + prisma.workout.findMany({ + where: { userId }, + orderBy: [{ startedAt: "asc" }, { source: "asc" }], + select: { id: true, source: true, externalId: true, sportType: true }, + }), + prisma.personalRecord.findMany({ + where: { userId }, + orderBy: { metricSlot: "asc" }, + select: { metricSlot: true, value: true, externalId: true, source: true }, + }), + ]); + return { workouts, records }; +} + +async function step( + name: string, + call: { + method: "GET" | "DELETE"; + id: string; + query?: Record; + auth?: RouteAuth; + readBack?: boolean; + }, +): Promise { + wideEventRecorder.take(); + effectRecorder.take(); + const res = await callRoute(call.method === "GET" ? GET : DELETE, { + method: call.method, + route: "/api/workouts/{id}", + params: { id: call.id }, + query: call.query, + auth: call.auth ?? owner, + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + await expectParity( + `workout-detail.${name}`, + stable( + { + status: res.status, + body: res.body, + events: events.map(eventFacts), + effects: effectRecorder.take(), + ...(call.readBack ? { stored: await stored() } : {}), + }, + { since: NOW }, + ), + ); +} + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const user = await makeUser({ + timezone: "Europe/Berlin", + dateOfBirth: new Date("1980-05-01T00:00:00.000Z"), + storeWorkoutRoutes: true, + }); + userId = user.id; + owner = await makeSession(user); + stranger = await makeSession(await makeUser({ timezone: "UTC" })); + + const res = await callRoute(postBatch, { + method: "POST", + route: "/api/workouts/batch", + auth: owner, + body: { + workouts: [ + workout("hk-run-1", "2026-10-14T06:30:00.000Z", 16, { + totalDistanceM: 3_200, + totalEnergyKcal: 260, + avgHeartRate: 146, + maxHeartRate: 158, + samples: SERIES, + route: TRACK, + }), + workout("hk-run-0", "2026-10-07T06:30:00.000Z", 30, { + totalDistanceM: 5_000, + avgHeartRate: 140, + }), + workout("hk-ride-1", "2026-10-12T16:00:00.000Z", 60, { + sportType: "cycling", + totalDistanceM: 24_000, + }), + workout("manual:walk-1", "2026-10-10T18:00:00.000Z", 40, { + sportType: "walking", + source: "MANUAL", + totalDistanceM: 3_000, + }), + ], + }, + }); + expect(res.status).toBe(200); + // The same run typed in by hand: a twin the canonical picker hides. + const twin = await prisma.workout.create({ + data: { + userId, + sportType: "running", + startedAt: new Date("2026-10-14T06:30:20.000Z"), + endedAt: new Date("2026-10-14T06:46:20.000Z"), + durationSec: 960, + source: "MANUAL", + externalId: "manual:run-twin", + totalDistanceM: 3_150, + }, + }); + ids.twin = twin.id; + ids.run = await idOf("hk-run-1"); + ids.ride = await idOf("hk-ride-1"); + ids.walk = await idOf("manual:walk-1"); + await prisma.personalRecord.create({ + data: { + userId, + metricType: "WALKING_RUNNING_DISTANCE", + metricSlot: "longest_distance_run", + direction: "MAX", + value: 3_000, + unit: "m", + achievedAt: new Date("2026-10-10T18:00:00.000Z"), + source: "MANUAL", + externalId: "manual:walk-1", + }, + }); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("GET /api/workouts/{id} parity", () => { + it("the detail reads", async () => { + await step("get-01-canonical", { method: "GET", id: ids.run }); + await step("get-02-compact", { + method: "GET", + id: ids.run, + query: { compact: "1" }, + }); + await step("get-03-hidden-twin", { method: "GET", id: ids.twin }); + await step("get-04-ride", { method: "GET", id: ids.ride }); + await step("get-05-other-account", { + method: "GET", + id: ids.run, + auth: stranger, + }); + await step("get-06-missing", { + method: "GET", + id: "cmissingworkout000000000", + }); + }); + + it("the module switched off", async () => { + await prisma.user.update({ + where: { id: userId }, + data: { modulePreferencesJson: { workouts: false } }, + }); + await step("get-07-module-off", { method: "GET", id: ids.run }); + await prisma.user.update({ + where: { id: userId }, + data: { modulePreferencesJson: {} }, + }); + }); +}); + +describe("DELETE /api/workouts/{id} parity", () => { + it("a synced workout, a missing one, another account's", async () => { + await step("delete-01-synced", { + method: "DELETE", + id: ids.ride, + readBack: true, + }); + await step("delete-02-missing", { + method: "DELETE", + id: "cmissingworkout000000000", + }); + await step("delete-03-other-account", { + method: "DELETE", + id: ids.walk, + auth: stranger, + readBack: true, + }); + }); + + it("a hand-entered workout and the record it set", async () => { + await step("delete-04-manual", { + method: "DELETE", + id: ids.walk, + readBack: true, + }); + }); +}); diff --git a/tests/integration/status-generator-parity.test.ts b/tests/integration/status-generator-parity.test.ts index 03968a913..9b34eddbe 100644 --- a/tests/integration/status-generator-parity.test.ts +++ b/tests/integration/status-generator-parity.test.ts @@ -503,7 +503,27 @@ async function record(arms: Record): Promise { notes: await storedNotes(), }; } - return `${JSON.stringify(out, null, 2)}\n`; + return `${JSON.stringify(sortedKeys(out), null, 2)}\n`; +} + +/** + * A copy with every object's keys in sorted order, so a golden's text does + * not depend on the order code happened to assign fields in and compresses + * and diffs well. Arrays keep their order: that is content. + */ +function sortedKeys(value: unknown): unknown { + if (Array.isArray(value)) return value.map(sortedKeys); + if (value && typeof value === "object" && !(value instanceof Date)) { + return Object.fromEntries( + Object.keys(value as Record) + .sort() + .map((key) => [ + key, + sortedKeys((value as Record)[key]), + ]), + ); + } + return value; } function compareGolden(name: string, actual: string): void { diff --git a/tests/kit/call-route.ts b/tests/kit/call-route.ts index d4ac7f0b6..b81e2bd74 100644 --- a/tests/kit/call-route.ts +++ b/tests/kit/call-route.ts @@ -64,6 +64,12 @@ export interface CallRouteOptions { * picks. */ body?: unknown; + /** + * A raw text body sent as it is (a CSV upload), with + * `content-type: text/plain` unless `headers` names another. Not combined + * with `body`. + */ + text?: string; auth: RouteAuth; headers?: Record; contract?: { skip: string }; @@ -97,7 +103,13 @@ export async function callRoute( if (value !== undefined) url.searchParams.set(key, String(value)); } + if (options.text !== undefined && options.body !== undefined) { + throw new Error("callRoute: pass either `body` or `text`, not both"); + } const headers = new Headers(options.headers); + if (options.text !== undefined && !headers.has("content-type")) { + headers.set("content-type", "text/plain"); + } const multipart = options.body instanceof FormData; if ( options.body !== undefined && @@ -132,11 +144,13 @@ export async function callRoute( method: options.method, headers, body: - options.body === undefined - ? undefined - : options.body instanceof FormData - ? options.body - : JSON.stringify(options.body), + options.text !== undefined + ? options.text + : options.body === undefined + ? undefined + : options.body instanceof FormData + ? options.body + : JSON.stringify(options.body), }); response = await handler(request, { params: Promise.resolve(options.params ?? {}), @@ -153,6 +167,10 @@ export async function callRoute( const isJson = contentType.includes("json") && text.length > 0; const body: unknown = isJson ? JSON.parse(text) : text; + // Not skippable with `contract`: a sealed column leaving the server is a + // defect whatever the contract says, so no test can opt a route out of it. + if (isJson) assertNoSealedFields(options.method, options.route, body); + if (!options.contract?.skip) { checkContract( options.method, @@ -182,6 +200,51 @@ export async function callRoute( }; } +/** + * A column the server stores sealed (`notesEncrypted`, `geometryEncrypted`, + * every `*Encrypted` field) is decrypted or dropped before a response is + * built. One that reaches a response anyway arrives as its key or as the + * serialised bytes (`{"0":…,"1":…}`, or `{ type: "Buffer", data: […] }`). + * Either fails the call, on every route the kit drives. + */ +export function sealedFieldPaths(value: unknown, at = ""): string[] { + if (Array.isArray(value)) { + return value.flatMap((item, i) => sealedFieldPaths(item, `${at}[${i}]`)); + } + if (!isRecord(value)) return []; + const keys = Object.keys(value); + // A sealed value is at least a nonce and a tag (28 bytes), so a short + // index-keyed object (an hour map, a weekday tally) is not mistaken for one. + const bufferShape = + (value.type === "Buffer" && Array.isArray(value.data)) || + (keys.length >= 16 && + keys.every((k, i) => k === String(i)) && + Object.values(value).every( + (v) => + Number.isInteger(v) && (v as number) >= 0 && (v as number) <= 255, + )); + if (bufferShape) return [at || "(body)"]; + return keys.flatMap((key) => { + const path = at ? `${at}.${key}` : key; + return /encrypted$/i.test(key) + ? [path] + : sealedFieldPaths(value[key], path); + }); +} + +function assertNoSealedFields( + method: HttpMethod, + route: string, + body: unknown, +): void { + const found = sealedFieldPaths(body); + if (found.length > 0) { + throw new ContractViolation( + `${method} ${route} sent sealed or raw byte fields to the client: ${found.join(", ")}. Decrypt onto the readable field or drop the column before the response is built.`, + ); + } +} + function concretePath(route: string, params: RouteParams): string { const used = new Set(); const path = route.replace(/\{([^}]+)\}/g, (_, name: string) => { diff --git a/tests/kit/parity.ts b/tests/kit/parity.ts index 07bd7641a..a778ace23 100644 --- a/tests/kit/parity.ts +++ b/tests/kit/parity.ts @@ -101,6 +101,57 @@ export function stable(value: unknown, options: StableOptions): unknown { return walk(value, null); } +const DAY_MS = 86_400_000; +const ANCHORED_INSTANT = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g; +const ANCHORED_DAY_KEY = /\b\d{4}-\d{2}-\d{2}\b/g; + +/** The start of the current UTC day, the anchor `relativeToAnchor` uses. */ +export function startOfUtcToday(): Date { + return new Date(Math.floor(Date.now() / DAY_MS) * DAY_MS); +} + +/** + * A copy of `value` with every ISO instant written as days and time from + * `anchor` (`anchor-13d+06:30:00`) and every `YYYY-MM-DD` day key as days from + * the anchor's day (`day-13`), inside any string. + * + * For a walk whose route reads windows against the database clock, which a + * pinned `Date` cannot reach: the record is laid out from the start of the + * current UTC day, the clock is pinned to that day, and the snapshot holds + * only positions relative to it, so it reads the same on every day it runs. + * Keep the account in UTC, so a daylight-saving change between two runs + * cannot move a local day boundary against the anchor. + */ +export function relativeToAnchor(value: unknown, anchor: Date): unknown { + const base = anchor.getTime(); + const instant = (iso: string) => { + const ms = Date.parse(iso) - base; + const days = Math.floor(ms / DAY_MS); + const time = new Date(ms - days * DAY_MS).toISOString().slice(11, 19); + return `anchor${days >= 0 ? "+" : ""}${days}d+${time}`; + }; + const dayKey = (key: string) => { + const days = Math.round((Date.parse(`${key}T00:00:00Z`) - base) / DAY_MS); + return `day${days >= 0 ? "+" : ""}${days}`; + }; + const walk = (node: unknown): unknown => { + if (node instanceof Date) node = node.toISOString(); + if (typeof node === "string") { + return node + .replace(ANCHORED_INSTANT, instant) + .replace(ANCHORED_DAY_KEY, dayKey); + } + if (Array.isArray(node)) return node.map(walk); + if (node && typeof node === "object" && !(node instanceof Uint8Array)) { + return Object.fromEntries( + Object.entries(node).map(([k, v]) => [k, walk(v)]), + ); + } + return node; + }; + return walk(value); +} + /** * The wide events requests emitted, collected in order. Install with * `vi.mock("@/lib/logging/transports", wideEventRecorder.mockFactory)` in